eyeprolog 1.5.95 → 1.5.97

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/README.md CHANGED
@@ -96,8 +96,7 @@ npm test
96
96
  The npm command list is deliberately small:
97
97
 
98
98
  - `npm test` (or `npm run test`): run the release gate, including live upstream conformity checks (WG17 syntax among them).
99
- - `npm run generate`: rebuild generated library and book files.
100
99
 
101
- Use `npm test -- --offline` for a network-free local pass (this also skips the live-discovered WG17 syntax check, since it has no offline snapshot). Focused checks remain available directly, for example `node test/run-regression.mjs docs`; see [test runners](test/README.md). The automatic version hooks still run the release gate, refresh and stage conformance reports, and push the release. Detailed upstream report maintenance is documented in the [conformance guide](test/conformance/README.md).
100
+ Use `npm test -- --offline` for a network-free local pass (this also skips the live-discovered WG17 syntax check, since it has no offline snapshot). Focused checks remain available directly, for example `node test/run-regression.mjs docs`; see [test runners](test/README.md). The automatic version hooks rebuild generated library and book files, run the release gate, refresh and stage conformance reports, and push the release. Detailed upstream report maintenance is documented in the [conformance guide](test/conformance/README.md).
102
101
 
103
102
  EyeProlog is released under the [MIT License](LICENSE.md).
@@ -8,9 +8,11 @@ not extracted.
8
8
  Regenerate them from the repository root with:
9
9
 
10
10
  ```sh
11
- npm run generate
11
+ node tools/extract-book-examples.mjs
12
12
  ```
13
13
 
14
+ `npm version` regenerates this directory automatically as part of its release checks.
15
+
14
16
  ## Chapter 1: A program is a little theory
15
17
 
16
18
  - [01-parent.pl](chapter-01/01-parent.pl)
@@ -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.95",
6
+ "version": "1.5.97",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -46,8 +46,7 @@
46
46
  },
47
47
  "scripts": {
48
48
  "test": "node test/run-all.mjs",
49
- "generate": "node tools/generate-library-autoload-index.mjs && node tools/generate-predicate-reference.mjs && node tools/extract-book-examples.mjs",
50
- "preversion": "npm test && node test/run-neumerkel.mjs --cached --update-report && node test/run-conformance-report.mjs conformance-report.md && git add test/conformance/NEUMERKEL-LATEST.md conformance-report.md",
49
+ "preversion": "node tools/generate-library-autoload-index.mjs && node tools/generate-predicate-reference.mjs && node tools/extract-book-examples.mjs && npm test && node test/run-neumerkel.mjs --cached --update-report && node test/run-conformance-report.mjs conformance-report.md && git add src/library-autoload-index.js the-art-of-eyeprolog.md examples/book test/conformance/NEUMERKEL-LATEST.md conformance-report.md",
51
50
  "postversion": "git push origin HEAD --follow-tags"
52
51
  }
53
52
  }
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",
@@ -629,7 +629,7 @@ ${profile}`;
629
629
  const publishIndex = publishWorkflow.indexOf('run: npm publish');
630
630
  assertEqual(testIndex >= 0 && testIndex < publishIndex, true, 'publish workflow test gate');
631
631
  assertEqual(packIndex >= 0 && packIndex < publishIndex, true, 'publish workflow package gate');
632
- assertArrayEqual(Object.keys(pkg.scripts).sort(), ['generate', 'postversion', 'preversion', 'test'], 'small npm command surface');
632
+ assertArrayEqual(Object.keys(pkg.scripts).sort(), ['postversion', 'preversion', 'test'], 'small npm command surface');
633
633
  assertEqual(pkg.scripts.test, 'node test/run-all.mjs', 'full release gate');
634
634
  const runner = fs.readFileSync(path.join(packageRoot, 'test', 'run-all.mjs'), 'utf8');
635
635
  assertIncludes(runner, 'runOpenRuleBenchChecks(reporter)', 'OpenRuleBench remains in release gate');
@@ -126,12 +126,18 @@ export function runNeumerkelHarnessTests(reporter = new TestReporter()) {
126
126
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
127
127
  const scripts = pkg.scripts ?? {};
128
128
  const releaseSteps = String(scripts.preversion ?? '').split(' && ');
129
- if (releaseSteps[0] !== 'npm test' || releaseSteps[1] !== 'node test/run-neumerkel.mjs --cached --update-report') {
129
+ if (releaseSteps[0] !== 'node tools/generate-library-autoload-index.mjs' ||
130
+ releaseSteps[1] !== 'node tools/generate-predicate-reference.mjs' ||
131
+ releaseSteps[2] !== 'node tools/extract-book-examples.mjs') {
132
+ throw new Error('preversion must regenerate the library index, predicate reference, and book examples first');
133
+ }
134
+ if (releaseSteps[3] !== 'npm test' || releaseSteps[4] !== 'node test/run-neumerkel.mjs --cached --update-report') {
130
135
  throw new Error('preversion must synchronize the tracked report from the successful npm test snapshot');
131
136
  }
132
- if (releaseSteps[2] !== 'node test/run-conformance-report.mjs conformance-report.md' ||
133
- releaseSteps[3] !== 'git add test/conformance/NEUMERKEL-LATEST.md conformance-report.md' || releaseSteps.length !== 4) {
134
- throw new Error('preversion must generate and stage both reports without another fetch');
137
+ if (releaseSteps[5] !== 'node test/run-conformance-report.mjs conformance-report.md' ||
138
+ releaseSteps[6] !== 'git add src/library-autoload-index.js the-art-of-eyeprolog.md examples/book ' +
139
+ 'test/conformance/NEUMERKEL-LATEST.md conformance-report.md' || releaseSteps.length !== 7) {
140
+ throw new Error('preversion must generate and stage the library index, book files, and both reports without another fetch');
135
141
  }
136
142
  });
137
143
 
@@ -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.
@@ -86,9 +86,11 @@ const readme = [
86
86
  'Regenerate them from the repository root with:',
87
87
  '',
88
88
  '```sh',
89
- 'npm run generate',
89
+ 'node tools/extract-book-examples.mjs',
90
90
  '```',
91
91
  '',
92
+ '`npm version` regenerates this directory automatically as part of its release checks.',
93
+ '',
92
94
  ];
93
95
 
94
96
  let total = 0;
@@ -97,7 +97,7 @@ const text = `// Generated by tools/generate-library-autoload-index.mjs.\n` +
97
97
  if (process.argv.includes('--check')) {
98
98
  const current = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf8') : '';
99
99
  if (current !== text) {
100
- console.error(`${path.relative(root, outputFile)} is stale; run npm run generate`);
100
+ console.error(`${path.relative(root, outputFile)} is stale; run node tools/generate-library-autoload-index.mjs (npm version regenerates it automatically)`);
101
101
  process.exit(1);
102
102
  }
103
103
  console.log(`${path.relative(root, outputFile)} is up to date`);
@@ -209,7 +209,7 @@ const wanted = expectedBook(book, generated);
209
209
 
210
210
  if (process.argv.includes('--check')) {
211
211
  if (wanted !== book) {
212
- console.error(`${path.relative(root, bookFile)} predicate reference is stale; run npm run generate`);
212
+ console.error(`${path.relative(root, bookFile)} predicate reference is stale; run node tools/generate-predicate-reference.mjs (npm version regenerates it automatically)`);
213
213
  process.exit(1);
214
214
  }
215
215
  console.log(`${path.relative(root, bookFile)} predicate reference is up to date (${surface.indicators.length} predicates)`);