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.
@@ -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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.96",
6
+ "version": "1.5.98",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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
- if (item.args[0].type !== VAR || approximateDecimalInterval(item.args[1]) == null) leaf.malformed ??= item;
364
- else leaf.approximations.push(item);
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
  `);
@@ -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 **229 self-contained runnable programs**. Every
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 229 normal answer goldens and the 61 selected proof goldens with:
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 229 normal examples and 61 proof examples; all executable chapter programs are parsed and their declared goals are executed. The nine-case
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.