eyeprolog 1.5.96 → 1.5.98
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/examples/declarative-fault-localization.pl +36 -0
- package/examples/knights-tour-warnsdorff.pl +46 -0
- package/examples/output/declarative-fault-localization.pl +1 -0
- package/examples/output/knights-tour-warnsdorff.pl +1 -0
- package/examples/output/reified-conditionals.pl +1 -0
- package/examples/output/universal-vs-existential-termination.pl +1 -0
- package/examples/output/vanilla-meta-interpreter.pl +1 -0
- package/examples/reified-conditionals.pl +20 -0
- package/examples/universal-vs-existential-termination.pl +29 -0
- package/examples/vanilla-meta-interpreter.pl +30 -0
- package/package.json +1 -1
- package/playground.html +5 -0
- package/src/quads.js +21 -2
- package/test/regression/cases-regression.mjs +20 -0
- package/the-art-of-eyeprolog.md +3 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
% Declarative fault localization: when a predicate returns a wrong answer,
|
|
2
|
+
% test it again on a strictly smaller sub-goal that a correct version would
|
|
3
|
+
% still have to get right. Each step either still shows the wrong answer
|
|
4
|
+
% (the fault is at or before this depth -- keep shrinking) or the sub-goal
|
|
5
|
+
% is fine (the fault must be in what combines that correct sub-result with
|
|
6
|
+
% the rest). No stack trace or print statement is needed: only whether each
|
|
7
|
+
% successively smaller call is itself correct.
|
|
8
|
+
%
|
|
9
|
+
% buggy_list_max/2 has its comparison branches swapped in the recursive
|
|
10
|
+
% clause (a classic off-by-one-style slip), so it sometimes reports the
|
|
11
|
+
% smaller of two candidates as the maximum.
|
|
12
|
+
buggy_list_max([X], X).
|
|
13
|
+
buggy_list_max([X|Xs], Max) :-
|
|
14
|
+
buggy_list_max(Xs, Max0),
|
|
15
|
+
( X > Max0 -> Max = Max0 ; Max = X ).
|
|
16
|
+
|
|
17
|
+
fixed_list_max([X], X).
|
|
18
|
+
fixed_list_max([X|Xs], Max) :-
|
|
19
|
+
fixed_list_max(Xs, Max0),
|
|
20
|
+
( X > Max0 -> Max = X ; Max = Max0 ).
|
|
21
|
+
|
|
22
|
+
%% goal: answer(X0, X1, X2, X3)
|
|
23
|
+
|
|
24
|
+
answer(WholeListWrongAnswer, TwoElementSublistAlreadyWrong, BaseCaseIsCorrect, FixedAnswer) :-
|
|
25
|
+
% The full query already gives the wrong maximum...
|
|
26
|
+
buggy_list_max([1, 5, 3], WholeListWrongAnswer),
|
|
27
|
+
% ...and so, it turns out, does the very first recursive step alone:
|
|
28
|
+
% this two-element call is already wrong on its own, which localizes
|
|
29
|
+
% the fault to the recursive clause itself, not to how its result
|
|
30
|
+
% later combines with the outer 1.
|
|
31
|
+
buggy_list_max([5, 3], TwoElementSublistAlreadyWrong),
|
|
32
|
+
% Shrinking one step further reaches the base case, which is correct
|
|
33
|
+
% (trivially): the fault cannot be shrunk past this depth.
|
|
34
|
+
buggy_list_max([3], BaseCaseIsCorrect),
|
|
35
|
+
% Swapping the branches back gives the right answer throughout.
|
|
36
|
+
fixed_list_max([1, 5, 3], FixedAnswer).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
% Knight's tour via Warnsdorff's rule: at each step, move to the reachable
|
|
2
|
+
% unvisited square with the fewest onward moves of its own. Preferring the
|
|
3
|
+
% most constrained square first empties the "hard" corners early, while
|
|
4
|
+
% plenty of freedom remains to reach them -- so a full tour is usually found
|
|
5
|
+
% without ever needing to backtrack. keysort/2 on Degree-Square pairs picks
|
|
6
|
+
% the least-constrained-first candidate; the cut in tour_/4's base case
|
|
7
|
+
% commits to the tour once every square is visited.
|
|
8
|
+
:- use_module(library(lists)).
|
|
9
|
+
|
|
10
|
+
board_size(5).
|
|
11
|
+
|
|
12
|
+
knight_move(p(X0, Y0), p(X, Y)) :-
|
|
13
|
+
member(d(DX, DY), [
|
|
14
|
+
d(1, 2), d(2, 1), d(-1, 2), d(-2, 1),
|
|
15
|
+
d(1, -2), d(2, -1), d(-1, -2), d(-2, -1)
|
|
16
|
+
]),
|
|
17
|
+
X is X0 + DX, Y is Y0 + DY,
|
|
18
|
+
board_size(N),
|
|
19
|
+
X >= 1, X =< N, Y >= 1, Y =< N.
|
|
20
|
+
|
|
21
|
+
degree(Square, Visited, Degree) :-
|
|
22
|
+
findall(Next, (knight_move(Square, Next), \+ member(Next, Visited)), Nexts),
|
|
23
|
+
length(Nexts, Degree).
|
|
24
|
+
|
|
25
|
+
tour_(_, Visited, SquareCount, Visited) :- length(Visited, SquareCount), !.
|
|
26
|
+
tour_(Current, Visited, SquareCount, Tour) :-
|
|
27
|
+
findall(Degree-Next, (
|
|
28
|
+
knight_move(Current, Next),
|
|
29
|
+
\+ member(Next, Visited),
|
|
30
|
+
degree(Next, Visited, Degree)
|
|
31
|
+
), Candidates),
|
|
32
|
+
Candidates \= [],
|
|
33
|
+
keysort(Candidates, [_-Best | _]),
|
|
34
|
+
tour_(Best, [Best | Visited], SquareCount, Tour).
|
|
35
|
+
|
|
36
|
+
knights_tour(Start, Tour) :-
|
|
37
|
+
board_size(N),
|
|
38
|
+
SquareCount is N * N,
|
|
39
|
+
tour_(Start, [Start], SquareCount, ReverseTour),
|
|
40
|
+
reverse(ReverseTour, Tour).
|
|
41
|
+
|
|
42
|
+
%% goal: answer(X0, X1)
|
|
43
|
+
|
|
44
|
+
answer(SquareCount, Tour) :-
|
|
45
|
+
knights_tour(p(1, 1), Tour),
|
|
46
|
+
length(Tour, SquareCount).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
answer(1, 3, 3, 5).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
answer(25, [p(1, 1), p(2, 3), p(1, 5), p(3, 4), p(5, 5), p(4, 3), p(5, 1), p(3, 2), p(4, 4), p(2, 5), p(1, 3), p(2, 1), p(4, 2), p(5, 4), p(3, 5), p(1, 4), p(2, 2), p(4, 1), p(5, 3), p(4, 5), p(2, 4), p(1, 2), p(3, 3), p(5, 2), p(3, 1)]).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
answer([2, 4, 6, 8], [1, 3, 5, 7], even, odd).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
answer([[] - "abc", "a" - "bc", "ab" - "c", "abc" - []], 4, true).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
answer([ann, pat], [jim]).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
% Reified conditionals: a "reified" predicate turns a condition into an
|
|
2
|
+
% ordinary term (true or false) that a caller receives as data, instead of
|
|
3
|
+
% committing to one branch itself the way (->)/2 does. if_/3 then dispatches
|
|
4
|
+
% on that term. The payoff is that reusable control predicates like
|
|
5
|
+
% tfilter/3 and tpartition/4 can be built once, on top of if_/3, and applied
|
|
6
|
+
% to any reified condition -- filtering and partitioning are not each
|
|
7
|
+
% reimplemented per predicate the way they would be with hand-written
|
|
8
|
+
% (->)/2 chains.
|
|
9
|
+
:- use_module(library(reif)).
|
|
10
|
+
|
|
11
|
+
even_t(N, true) :- 0 is N mod 2, !.
|
|
12
|
+
even_t(_, false).
|
|
13
|
+
|
|
14
|
+
%% goal: answer(X0, X1, X2, X3)
|
|
15
|
+
|
|
16
|
+
answer(Evens, Odds, First, Second) :-
|
|
17
|
+
tfilter(even_t, [1, 2, 3, 4, 5, 6, 7, 8], Evens),
|
|
18
|
+
tpartition(even_t, [1, 2, 3, 4, 5, 6, 7, 8], _, Odds),
|
|
19
|
+
if_(even_t(4), First = even, First = odd),
|
|
20
|
+
if_(even_t(7), Second = even, Second = odd).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
% Universal vs. existential termination: a query terminates *existentially*
|
|
2
|
+
% if finding one answer (or failing outright) takes finite time; it
|
|
3
|
+
% terminates *universally* only if exhausting every answer does. The two
|
|
4
|
+
% can come apart for the very same predicate, depending only on how it is
|
|
5
|
+
% called -- append/3 is the classic case.
|
|
6
|
+
%
|
|
7
|
+
% append(X, Y, [a,b,c]) has exactly as many solutions as there are ways to
|
|
8
|
+
% split a 3-element list, so asking for all of them (findall/3) terminates:
|
|
9
|
+
% this call terminates universally.
|
|
10
|
+
%
|
|
11
|
+
% append(X, Y, Z) with Z left unbound has infinitely many solutions (Z can
|
|
12
|
+
% be a list of any length), so the first one is still found immediately
|
|
13
|
+
% (existential termination holds), but findall/3 over it would never
|
|
14
|
+
% return: this call terminates existentially but not universally. once/1
|
|
15
|
+
% below deliberately stops after that first solution so this file's own
|
|
16
|
+
% checked output stays finite -- do not replace it with findall/3.
|
|
17
|
+
|
|
18
|
+
%% goal: answer(X0, X1, X2)
|
|
19
|
+
|
|
20
|
+
answer(AllSplitsOfBoundList, SplitCount, FoundFirstSplitOfUnboundList) :-
|
|
21
|
+
findall(X - Y, append(X, Y, [a, b, c]), AllSplitsOfBoundList),
|
|
22
|
+
length(AllSplitsOfBoundList, SplitCount),
|
|
23
|
+
% X2 comes out ground ([]), but Y2 and Z2 stay unbound (merely aliased
|
|
24
|
+
% to each other) -- append/3's first solution names no particular
|
|
25
|
+
% list at all, only that Y and Z must coincide. Reporting that
|
|
26
|
+
% once/1 succeeded is itself the point: existential termination only
|
|
27
|
+
% promises a first answer exists, not that it pins everything down.
|
|
28
|
+
once((append(X2, _Y2, _Z2), X2 == [])),
|
|
29
|
+
FoundFirstSplitOfUnboundList = true.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
% A vanilla meta-interpreter: solve/1 mirrors the ordinary Prolog resolution
|
|
2
|
+
% rule at the object level, using clause/2 to fetch a matching clause and
|
|
3
|
+
% conjunction to solve its body. It shows that "how Prolog executes a goal"
|
|
4
|
+
% is itself expressible as an ordinary Prolog relation, not a hidden
|
|
5
|
+
% mechanism -- the classic starting point for building custom reasoners
|
|
6
|
+
% (tracers, proof recorders, alternative search strategies) on top of plain
|
|
7
|
+
% Prolog. clause/2 only inspects dynamic procedures, hence the declarations.
|
|
8
|
+
:- dynamic(parent/2).
|
|
9
|
+
:- dynamic(grandparent/2).
|
|
10
|
+
:- dynamic(great_grandparent/2).
|
|
11
|
+
|
|
12
|
+
solve(true) :- !.
|
|
13
|
+
solve((A, B)) :- !, solve(A), solve(B).
|
|
14
|
+
solve(Goal) :-
|
|
15
|
+
clause(Goal, Body),
|
|
16
|
+
solve(Body).
|
|
17
|
+
|
|
18
|
+
parent(tom, bob).
|
|
19
|
+
parent(bob, ann).
|
|
20
|
+
parent(bob, pat).
|
|
21
|
+
parent(pat, jim).
|
|
22
|
+
|
|
23
|
+
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
|
|
24
|
+
great_grandparent(X, Z) :- parent(X, Y), grandparent(Y, Z).
|
|
25
|
+
|
|
26
|
+
%% goal: answer(X0, X1)
|
|
27
|
+
|
|
28
|
+
answer(Grandchildren, GreatGrandchildren) :-
|
|
29
|
+
findall(X, solve(grandparent(tom, X)), Grandchildren),
|
|
30
|
+
findall(X, solve(great_grandparent(tom, X)), GreatGrandchildren).
|
package/package.json
CHANGED
package/playground.html
CHANGED
|
@@ -493,6 +493,7 @@
|
|
|
493
493
|
"data-negotiation",
|
|
494
494
|
"dcg-command-parser",
|
|
495
495
|
"dcg-expression-language",
|
|
496
|
+
"declarative-fault-localization",
|
|
496
497
|
"deep-taxonomy-10",
|
|
497
498
|
"deep-taxonomy-100",
|
|
498
499
|
"deep-taxonomy-1000",
|
|
@@ -571,6 +572,7 @@
|
|
|
571
572
|
"job-shop-scheduling",
|
|
572
573
|
"json",
|
|
573
574
|
"knapsack-optimization",
|
|
575
|
+
"knights-tour-warnsdorff",
|
|
574
576
|
"knowledge-engineering-alignment-flow",
|
|
575
577
|
"knuth-bendix-completion",
|
|
576
578
|
"language",
|
|
@@ -627,6 +629,7 @@
|
|
|
627
629
|
"rdf12-trig-triple-term",
|
|
628
630
|
"rdf12-triple-term",
|
|
629
631
|
"register-allocation",
|
|
632
|
+
"reified-conditionals",
|
|
630
633
|
"relational-cube-lookup",
|
|
631
634
|
"reusable-builtins",
|
|
632
635
|
"riemann-hypothesis",
|
|
@@ -656,7 +659,9 @@
|
|
|
656
659
|
"truth-maintenance-system",
|
|
657
660
|
"turing",
|
|
658
661
|
"type-inference",
|
|
662
|
+
"universal-vs-existential-termination",
|
|
659
663
|
"uuid",
|
|
664
|
+
"vanilla-meta-interpreter",
|
|
660
665
|
"vector-similarity",
|
|
661
666
|
"vulnerability-impact",
|
|
662
667
|
"web-names",
|
package/src/quads.js
CHANGED
|
@@ -322,6 +322,7 @@ function describeLeaf(term) {
|
|
|
322
322
|
const leaf = {
|
|
323
323
|
bindings: [],
|
|
324
324
|
approximations: [],
|
|
325
|
+
literalApproximations: [],
|
|
325
326
|
unexpected: false,
|
|
326
327
|
more: false,
|
|
327
328
|
sto: false,
|
|
@@ -360,8 +361,21 @@ function describeLeaf(term) {
|
|
|
360
361
|
continue;
|
|
361
362
|
}
|
|
362
363
|
if (item.type === COMPOUND && item.name === '~~' && item.arity === 2) {
|
|
363
|
-
|
|
364
|
-
|
|
364
|
+
// The left-hand side names either one of the query's own variables
|
|
365
|
+
// (the usual case: compare what the query actually bound it to) or,
|
|
366
|
+
// just as legitimately, a literal number written directly in the
|
|
367
|
+
// answer description -- a free-standing "this value approximately
|
|
368
|
+
// denotes this decimal interval" claim with no query variable
|
|
369
|
+
// involved at all (issue #113: `0.04 ~~ '0.0'` was rejected as
|
|
370
|
+
// malformed purely for lacking a variable on the left, with no
|
|
371
|
+
// deeper reason for that restriction). Both are validated the same
|
|
372
|
+
// way one level up, in matchLeaf/substitutionMatches: syntactic
|
|
373
|
+
// acceptance here does not yet mean the value actually falls in the
|
|
374
|
+
// stated interval.
|
|
375
|
+
const lhsIsVarOrNumber = item.args[0].type === VAR || item.args[0].type === NUMBER;
|
|
376
|
+
if (!lhsIsVarOrNumber || approximateDecimalInterval(item.args[1]) == null) leaf.malformed ??= item;
|
|
377
|
+
else if (item.args[0].type === VAR) leaf.approximations.push(item);
|
|
378
|
+
else leaf.literalApproximations.push(item);
|
|
365
379
|
continue;
|
|
366
380
|
}
|
|
367
381
|
if (item.type === COMPOUND && item.name === 'inputs' && item.arity === 1) {
|
|
@@ -385,6 +399,7 @@ function describeLeaf(term) {
|
|
|
385
399
|
else leaf.malformed ??= item;
|
|
386
400
|
}
|
|
387
401
|
leaf.hasExpectation = leaf.bindings.length > 0 || leaf.approximations.length > 0 ||
|
|
402
|
+
leaf.literalApproximations.length > 0 ||
|
|
388
403
|
leaf.truth || leaf.false || leaf.maybe || leaf.loops || leaf.waits ||
|
|
389
404
|
leaf.error != null || leaf.output != null;
|
|
390
405
|
if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
|
|
@@ -528,6 +543,10 @@ function matchLeaf(program, query, leaf, actual, position) {
|
|
|
528
543
|
// its absence requires an unconstrained answer. In either case the stated
|
|
529
544
|
// substitutions remain exact and are checked below.
|
|
530
545
|
if (leaf.maybe !== hasPendingConstraints(solution.env)) return false;
|
|
546
|
+
// Literal `~~` claims (issue #113) name no query variable at all, so
|
|
547
|
+
// they carry no per-position runtime state to look up; they either hold
|
|
548
|
+
// or they don't, the same at every position.
|
|
549
|
+
if (leaf.literalApproximations.some((item) => !approximatelyMatches(item.args[0], item.args[1]))) return false;
|
|
531
550
|
return substitutionMatches(query, leaf.bindings, leaf.approximations, solution.env);
|
|
532
551
|
}
|
|
533
552
|
|
|
@@ -1195,6 +1195,26 @@ why(
|
|
|
1195
1195
|
assertEqual(malformedAtom.failed, 1, 'non-decimal atom is rejected');
|
|
1196
1196
|
assertIncludes(malformedAtom.stdout, 'MALFORMED', 'non-decimal atom diagnostic');
|
|
1197
1197
|
|
|
1198
|
+
// Issue #113: a literal number on the left of `~~` names no query
|
|
1199
|
+
// variable at all -- it is a free-standing "this value approximately
|
|
1200
|
+
// denotes this decimal interval" claim, and there is no deeper reason
|
|
1201
|
+
// to require a variable there. It must be accepted (and actually
|
|
1202
|
+
// checked against the interval, not accepted unconditionally), the
|
|
1203
|
+
// same as the query-variable form already was.
|
|
1204
|
+
const literalOk = publicApi.runQuads('?- true.\n 0.04 ~~ \'0.0\'.\n');
|
|
1205
|
+
assertEqual(literalOk.total, 1, 'literal approximation total');
|
|
1206
|
+
assertEqual(literalOk.passed, 1, 'literal approximation passed');
|
|
1207
|
+
assertEqual(literalOk.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'literal approximation report');
|
|
1208
|
+
|
|
1209
|
+
const literalOutOfRange = publicApi.runQuads('?- true.\n 0.04 ~~ \'5.0\'.\n');
|
|
1210
|
+
assertEqual(literalOutOfRange.failed, 1, 'literal outside the claimed interval fails, not malformed');
|
|
1211
|
+
assertIncludes(literalOutOfRange.stdout, 'quads: FAILED', 'literal out-of-range diagnostic');
|
|
1212
|
+
assertNotIncludes(literalOutOfRange.stdout, 'MALFORMED', 'literal out-of-range is not malformed');
|
|
1213
|
+
|
|
1214
|
+
const literalNonNumberLhs = publicApi.runQuads('?- true.\n foo ~~ \'0.0\'.\n');
|
|
1215
|
+
assertEqual(literalNonNumberLhs.failed, 1, 'a non-variable, non-number left side remains malformed');
|
|
1216
|
+
assertIncludes(literalNonNumberLhs.stdout, 'MALFORMED', 'non-number left side diagnostic');
|
|
1217
|
+
|
|
1198
1218
|
const duplicate = publicApi.runQuads(`?- V is 14.2.
|
|
1199
1219
|
V = 14.2, V ~~ '14.2000'.
|
|
1200
1220
|
`);
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -9934,7 +9934,7 @@ Review questions:
|
|
|
9934
9934
|
</figure>
|
|
9935
9935
|
|
|
9936
9936
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
9937
|
-
top-level directory contains **
|
|
9937
|
+
top-level directory contains **234 self-contained runnable programs**. Every
|
|
9938
9938
|
source program has an exact answer file under
|
|
9939
9939
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
|
|
9940
9940
|
explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic lists link every top-level program and open the program
|
|
@@ -10346,7 +10346,7 @@ hand.
|
|
|
10346
10346
|
|
|
10347
10347
|
#### Running and extending the corpus
|
|
10348
10348
|
|
|
10349
|
-
Run all
|
|
10349
|
+
Run all 234 normal answer goldens and the 61 selected proof goldens with:
|
|
10350
10350
|
|
|
10351
10351
|
```sh
|
|
10352
10352
|
node test/run-examples.mjs
|
|
@@ -10418,7 +10418,7 @@ accept texts outside the strict grammar, but it may not reinterpret an accepted
|
|
|
10418
10418
|
standard case.
|
|
10419
10419
|
|
|
10420
10420
|
The file-based conformance corpus contains 905 cases, including 479 focused ISO cases derived from the success, failure, mode, and error behavior in ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
10421
|
-
Separate exact-output suites check
|
|
10421
|
+
Separate exact-output suites check 234 normal examples and 61 proof examples; all executable chapter programs are parsed and their declared goals are executed. The nine-case
|
|
10422
10422
|
playground contract suite imports the production worker, sends real reasoning
|
|
10423
10423
|
requests through its message protocol, and crawls the served module graph for
|
|
10424
10424
|
missing assets, bad MIME types, and static Node-only imports. `conformance-report.md` inventories the file-based conformance corpus and links to the live Neumerkel evidence, which includes the current WG17 syntax result.
|