eyeprolog 0.5.12 → 0.5.13

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.
Files changed (38) hide show
  1. package/README.md +5 -3
  2. package/examples/age.pl +1 -1
  3. package/examples/blocks-world-planning.pl +3 -3
  4. package/examples/combinatorics-findall-sort.pl +4 -4
  5. package/examples/d3-group.pl +3 -3
  6. package/examples/dijkstra-findall-sort.pl +3 -3
  7. package/examples/enigma1225.pl +1 -2
  8. package/examples/eulerian-path.pl +4 -4
  9. package/examples/four-color-map.pl +2 -2
  10. package/examples/input/odrl-dpv-healthcare-risk-ranked-rules.pl +1 -1
  11. package/examples/input/odrl-dpv-risk-ranked-rules.pl +1 -1
  12. package/examples/input/rdf12-annotated-claims-rules.pl +1 -1
  13. package/examples/odrl-dpv-healthcare-risk-ranked.pl +1 -1
  14. package/examples/odrl-dpv-risk-ranked.pl +1 -1
  15. package/examples/proof/age.pl +1 -1
  16. package/examples/proof/d3-group.pl +4 -4
  17. package/examples/proof/iso-dynamic-database.pl +1 -1
  18. package/examples/quine-mccluskey.pl +7 -7
  19. package/examples/rdf12-annotated-claims.pl +1 -1
  20. package/examples/sudoku.pl +1 -1
  21. package/package.json +1 -1
  22. package/playground.html +27 -7
  23. package/src/cli.js +1 -1
  24. package/src/explain.js +1 -1
  25. package/src/{eyeprolog-library.js → eyeprolog-autoload.js} +23 -8
  26. package/src/eyeprolog-common-library.pl +44 -0
  27. package/src/eyeprolog-library.pl +49 -76
  28. package/src/index.js +2 -2
  29. package/src/playground-worker.js +2 -2
  30. package/src/solver.js +1 -1
  31. package/test/conformance/cases/lists/031_lists_aggregation_ordering.pl +1 -2
  32. package/test/conformance/cases/lists/045_sort_deduplicates_atoms.pl +2 -3
  33. package/test/conformance/cases/lists/087_sort_reverse_length.pl +2 -2
  34. package/test/conformance/cases/lists/extra_sort_iri_atoms.pl +1 -1
  35. package/test/conformance/cases/lists/sort_structured_terms.pl +1 -1
  36. package/test/run-playground.mjs +5 -1
  37. package/test/run-regression.mjs +6 -3
  38. package/the-art-of-eyeprolog.md +22 -17
package/README.md CHANGED
@@ -32,9 +32,11 @@ Programs may declare their default queries with `%% goal:` comments.
32
32
  ## Portable library
33
33
 
34
34
  EyeProlog adds 50 public library predicates to its 115-predicate ISO profile.
35
- **All 50 are ordinary Prolog clauses** in `src/eyeprolog-library.pl` and are
36
- autoloaded in Node and the browser. None requires host support. Portable text
37
- predicates use ISO atoms or character lists; the
35
+ **All 50 are ordinary Prolog clauses** across `src/eyeprolog-library.pl` and
36
+ `src/eyeprolog-common-library.pl`; both are autoloaded in Node and the
37
+ browser. None requires host support. Other Prolog systems should load only
38
+ `eyeprolog-library.pl`, which avoids redefining their common list predicates.
39
+ Portable text predicates use ISO atoms or character lists; the
38
40
  RDF tools emit lexical values as ISO atoms as well.
39
41
 
40
42
  ## RDF 1.2
package/examples/age.pl CHANGED
@@ -27,4 +27,4 @@ ageAbove(S, A) :-
27
27
 
28
28
  % Test mirroring the Eyeling example.
29
29
  holds_result(test, true) :-
30
- ageAbove(S, 'P80Y').
30
+ ageAbove(_, 'P80Y').
@@ -48,7 +48,7 @@ move(State, move(Block, From, To), Newstate) :-
48
48
  (Block \= To),
49
49
  (From \= To),
50
50
  select(on(Block, From), State, Rest),
51
- sort([on(Block, To)|Rest], Newstate).
51
+ sort_unique([on(Block, To)|Rest], Newstate).
52
52
 
53
53
  plan(State, Goal, 0, _visited, [], State) :-
54
54
  (State = Goal).
@@ -63,8 +63,8 @@ plan(State, Goal, Depth, Visited, [Move|Moves], Final) :-
63
63
  five_move_plan(Moves, Final) :-
64
64
  initial(Start),
65
65
  goal(Goal),
66
- sort(Start, Sortedstart),
67
- sort(Goal, Sortedgoal),
66
+ sort_unique(Start, Sortedstart),
67
+ sort_unique(Goal, Sortedgoal),
68
68
  plan(Sortedstart, Sortedgoal, 5, [Sortedstart], Moves, Final).
69
69
 
70
70
  status(blocks_world, planned) :-
@@ -1,7 +1,7 @@
1
- % Eyelet-inspired combinations example using findall/3 and sort/2.
1
+ % Eyelet-inspired combinations example using findall/3 and sort_unique/2.
2
2
  %
3
3
  % combination/3 generates the same subset in several selection orders. findall/3
4
- % collects those candidates, and sort/2 canonicalizes the list so each unordered
4
+ % collects those candidates, and sort_unique/2 canonicalizes the list so each unordered
5
5
  % 3-combination of five items is reported once.
6
6
  %% goal: combinations(X0, X1)
7
7
 
@@ -24,12 +24,12 @@ combination(I, Items, Combination) :-
24
24
  select(Item, Items, Remaining),
25
25
  (J is I - 1),
26
26
  combination(J, Remaining, Partial),
27
- sort([Item | Partial], Combination).
27
+ sort_unique([Item | Partial], Combination).
28
28
 
29
29
  % findall collects all generation orders; sort canonicalizes and deduplicates.
30
30
  unique_combinations(K, Items, Unique) :-
31
31
  findall(C, combination(K, Items, C), All),
32
- sort(All, Unique).
32
+ sort_unique(All, Unique).
33
33
 
34
34
  combinations(combinations_5_choose_3, Unique) :-
35
35
  unique_combinations(3, [1, 2, 3, 4, 5], Unique).
@@ -1,4 +1,4 @@
1
- % Eyelet-inspired D3 group example using findall/3 and sort/2.
1
+ % Eyelet-inspired D3 group example using findall/3 and sort_unique/2.
2
2
  % The six facts are the symmetries of an equilateral triangle, with compose/3 as
3
3
  % the Cayley table and inverse/2 as the inverse relation. Candidate subsets are
4
4
  % generated as subsequences, then filtered for subgroup closure.
@@ -72,7 +72,7 @@ subsequence([_head | Tail], Rest) :-
72
72
 
73
73
  all_symmetries(Symmetries) :-
74
74
  findall(X, symmetry(X), Raw),
75
- sort(Raw, Symmetries).
75
+ sort_unique(Raw, Symmetries).
76
76
 
77
77
  % A valid subgroup is closed under both composition and inverse.
78
78
  closed_under_composition(Group) :-
@@ -90,7 +90,7 @@ valid_group(Group) :-
90
90
 
91
91
  all_subgroups(Groups) :-
92
92
  findall(G, valid_group(G), Raw),
93
- sort(Raw, Groups).
93
+ sort_unique(Raw, Groups).
94
94
 
95
95
  subgroups(d3_group, Groups) :-
96
96
  all_subgroups(Groups).
@@ -1,7 +1,7 @@
1
- % Eyelet-inspired Dijkstra example using findall/3 and sort/2.
1
+ % Eyelet-inspired Dijkstra example using findall/3 and sort_unique/2.
2
2
  % The priority queue is represented as sorted list entries [Cost, Node | Path].
3
3
  % Each expansion collects unvisited neighbors with findall/3, appends them to
4
- % the frontier, and uses sort/2 so the cheapest frontier entry is processed next.
4
+ % the frontier, and uses sort_unique/2 so the cheapest frontier entry is processed next.
5
5
 
6
6
  %% goal: shortestPath(X0, X1)
7
7
 
@@ -34,7 +34,7 @@ dijkstra_queue([[Cost, Node | Path] | Queue], Goal, Visited, Resultpath, Resultc
34
34
  (edge(Node, Neighbor, Weight), \+ member(Neighbor, Visited), (Newcost is Cost + Weight)),
35
35
  Neighbors),
36
36
  append(Queue, Neighbors, Newqueue),
37
- sort(Newqueue, Sortedqueue),
37
+ sort_unique(Newqueue, Sortedqueue),
38
38
  dijkstra_queue(Sortedqueue, Goal, [Node | Visited], Resultpath, Resultcost).
39
39
 
40
40
  shortestPath(dijkstra_findall_sort, Path) :-
@@ -89,7 +89,7 @@ eval_matrix(Matrix, FreqSorted) :-
89
89
  setof(E, member(E, Entries), Set),
90
90
  maplist(count_var(Entries), Set, Multiplicities),
91
91
  zip(Multiplicities, Set, Frequencies),
92
- sort(Frequencies, FreqSorted),
92
+ sort_unique(Frequencies, FreqSorted),
93
93
  maplist(snd, FreqSorted, VarsSorted),
94
94
  length(VarsSorted, NVars),
95
95
  from_to(1, NVars, VarsSorted).
@@ -237,4 +237,3 @@ lists_reform([[A|B]|C], [A|D], [B|E]) :-
237
237
 
238
238
  % query
239
239
  %% goal: enigma1225(8, _)
240
-
@@ -1,4 +1,4 @@
1
- % Eyelet-inspired Eulerian path example using findall/3 and sort/2.
1
+ % Eyelet-inspired Eulerian path example using findall/3 and sort_unique/2.
2
2
  %
3
3
  % The graph is undirected; edges have identifiers so the trail consumes each
4
4
  % physical edge exactly once even when vertices are revisited. The remaining
@@ -49,15 +49,15 @@ odd_degree(V) :-
49
49
 
50
50
  odd_vertices(Odds) :-
51
51
  findall(V, odd_degree(V), Raw),
52
- sort(Raw, Odds).
52
+ sort_unique(Raw, Odds).
53
53
 
54
54
  all_edges(Edges) :-
55
55
  findall(E, edge(E, _a, _b), Raw),
56
- sort(Raw, Edges).
56
+ sort_unique(Raw, Edges).
57
57
 
58
58
  vertices(Vertices) :-
59
59
  findall(V, vertex(V), Raw),
60
- sort(Raw, Vertices).
60
+ sort_unique(Raw, Vertices).
61
61
 
62
62
  eulerian_start(Start) :-
63
63
  odd_vertices([Start, _end]).
@@ -102,13 +102,13 @@ border_count(36).
102
102
 
103
103
  all_countries_coloured(map_eu) :-
104
104
  findall(Country, valid_assignment(Country), Countries),
105
- sort(Countries, Uniquecountries),
105
+ sort_unique(Countries, Uniquecountries),
106
106
  length(Uniquecountries, Count),
107
107
  country_count(Count).
108
108
 
109
109
  all_borders_checked(map_eu) :-
110
110
  findall([A, B], border_colours_differ(A, B), Borders),
111
- sort(Borders, Uniqueborders),
111
+ sort_unique(Borders, Uniqueborders),
112
112
  length(Uniqueborders, Count),
113
113
  border_count(Count).
114
114
 
@@ -15,7 +15,7 @@ healthcare_risk_report(Ranked) :-
15
15
  ),
16
16
  Unsorted
17
17
  ),
18
- sort(Unsorted, Sorted),
18
+ sort_unique(Unsorted, Sorted),
19
19
  ranked_values(Sorted, 1, Ranked).
20
20
 
21
21
  risk_report(Risk, Score, Level, Clause, Mitigation) :-
@@ -15,7 +15,7 @@ consumer_risk_report(Ranked) :-
15
15
  ),
16
16
  Unsorted
17
17
  ),
18
- sort(Unsorted, Sorted),
18
+ sort_unique(Unsorted, Sorted),
19
19
  ranked_values(Sorted, 1, Ranked).
20
20
 
21
21
  risk_report(Risk, Score, Level, Clause, Mitigation) :-
@@ -25,7 +25,7 @@ ranked_claims(Claims) :-
25
25
  ),
26
26
  Unsorted
27
27
  ),
28
- sort(Unsorted, Sorted),
28
+ sort_unique(Unsorted, Sorted),
29
29
  pair_values(Sorted, Claims).
30
30
 
31
31
  annotated_status_claim(Status, Source, Score) :-
@@ -56,7 +56,7 @@ healthcare_risk_report(Ranked) :-
56
56
  ),
57
57
  Unsorted
58
58
  ),
59
- sort(Unsorted, Sorted),
59
+ sort_unique(Unsorted, Sorted),
60
60
  ranked_values(Sorted, 1, Ranked).
61
61
 
62
62
  risk_report(Risk, Score, Level, Clause, Mitigation) :-
@@ -68,7 +68,7 @@ consumer_risk_report(Ranked) :-
68
68
  ),
69
69
  Unsorted
70
70
  ),
71
- sort(Unsorted, Sorted),
71
+ sort_unique(Unsorted, Sorted),
72
72
  ranked_values(Sorted, 1, Ranked).
73
73
 
74
74
  risk_report(Risk, Score, Level, Clause, Mitigation) :-
@@ -36,7 +36,7 @@ why(
36
36
  proof(
37
37
  goal(holds_result(test, true)),
38
38
  by(rule("age.pl", clause(5))),
39
- bindings([binding("S", patH)]),
39
+ bindings([binding("__anon0", patH)]),
40
40
  uses([
41
41
  proof(
42
42
  goal(ageAbove(patH, 'P80Y')),
@@ -16,8 +16,8 @@ why(
16
16
  by(builtin(findall, 3))
17
17
  ),
18
18
  proof(
19
- goal(sort([], [])),
20
- by(library(sort, 2))
19
+ goal(sort_unique([], [])),
20
+ by(library(sort_unique, 2))
21
21
  )
22
22
  ])
23
23
  )
@@ -43,8 +43,8 @@ why(
43
43
  by(builtin(findall, 3))
44
44
  ),
45
45
  proof(
46
- goal(sort([], [])),
47
- by(library(sort, 2))
46
+ goal(sort_unique([], [])),
47
+ by(library(sort_unique, 2))
48
48
  )
49
49
  ])
50
50
  ),
@@ -12,7 +12,7 @@ why(
12
12
  uses([
13
13
  proof(
14
14
  goal(task(check_power, urgent)),
15
- by(fact("<input>", clause(299)))
15
+ by(fact("<input>", clause(303)))
16
16
  )
17
17
  ])
18
18
  )
@@ -75,7 +75,7 @@ check(4, 'Canonical Tie-Breaking (Lexicographical First)', Status) :-
75
75
  find_combination(Size, Primes, C),
76
76
  covers_all(C, Minterms)
77
77
  ), Alternatives),
78
- sort(Alternatives, [Best|_]),
78
+ sort_unique(Alternatives, [Best|_]),
79
79
  equality_status(Cover, Best, Status).
80
80
 
81
81
  check(5, 'Consistency (Solution is subset of Primes)', Status) :-
@@ -166,9 +166,9 @@ compute_primes(Primes) :-
166
166
  findall(B, (minterm(M), int_to_bits(M, B)), Mts),
167
167
  findall(B, (dont_care(D), int_to_bits(D, B)), Dcs),
168
168
  append(Mts, Dcs, All),
169
- sort(All, Init),
169
+ sort_unique(All, Init),
170
170
  generate_loop(Init, [], RawPrimes),
171
- sort(RawPrimes, Primes).
171
+ sort_unique(RawPrimes, Primes).
172
172
 
173
173
  generate_loop(Group, Acc, Final) :-
174
174
  findall(Nx-[P1,P2], (
@@ -176,8 +176,8 @@ generate_loop(Group, Acc, Final) :-
176
176
  ), Pairs),
177
177
  findall(N, member(N-_, Pairs), NextRaw),
178
178
  findall(P, (member(_-Pars, Pairs), member(P, Pars)), UsedRaw),
179
- sort(NextRaw, NextGen),
180
- sort(UsedRaw, Used),
179
+ sort_unique(NextRaw, NextGen),
180
+ sort_unique(UsedRaw, Used),
181
181
  findall(P, (member(P, Group), \+ member(P, Used)), New),
182
182
  append(Acc, New, AccUpd),
183
183
  (NextGen = [] -> Final = AccUpd ; generate_loop(NextGen, AccUpd, Final)).
@@ -187,13 +187,13 @@ solve_minimal_cover(Primes, MinimalCover) :-
187
187
  % 1. Essentials
188
188
  findall(M-Ps, (member(M, Minterms), findall(P, (member(P, Primes), covers_int(P, M)), Ps)), Chart),
189
189
  findall(P, member(_-[P], Chart), EssRaw),
190
- sort(EssRaw, Essentials),
190
+ sort_unique(EssRaw, Essentials),
191
191
  % 2. Remaining
192
192
  remaining_minterms(Minterms, Essentials, RemMts),
193
193
  subtract_list(Primes, Essentials, NonEss),
194
194
  (RemMts = [] -> BestRest = [] ; find_smallest_subset(NonEss, RemMts, BestRest)),
195
195
  append(Essentials, BestRest, Total),
196
- sort(Total, MinimalCover).
196
+ sort_unique(Total, MinimalCover).
197
197
 
198
198
  remaining_minterms([], _, []).
199
199
  remaining_minterms([M|Ms], Essentials, Rest) :-
@@ -41,7 +41,7 @@ ranked_claims(Claims) :-
41
41
  ),
42
42
  Unsorted
43
43
  ),
44
- sort(Unsorted, Sorted),
44
+ sort_unique(Unsorted, Sorted),
45
45
  pair_values(Sorted, Claims).
46
46
 
47
47
  annotated_status_claim(Status, Source, Score) :-
@@ -46,7 +46,7 @@ best_choice(Grid, Row, Column, Candidates) :-
46
46
  ),
47
47
  Choices
48
48
  ),
49
- sort(Choices, [choice(_Count, Row, Column, Candidates)|_]).
49
+ sort_unique(Choices, [choice(_Count, Row, Column, Candidates)|_]).
50
50
 
51
51
  empty_cell(Grid, Row, Column) :-
52
52
  between(0, 8, Row),
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.12",
6
+ "version": "0.5.13",
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
@@ -655,6 +655,7 @@
655
655
  "aggregate_max",
656
656
  "aggregate_min",
657
657
  "append",
658
+ "apply",
658
659
  "arg",
659
660
  "asin",
660
661
  "atan2",
@@ -707,7 +708,7 @@
707
708
  "sin",
708
709
  "slice",
709
710
  "smallest_divisor_from",
710
- "sort",
711
+ "sort_unique",
711
712
  "split",
712
713
  "sqrt",
713
714
  "string_concat",
@@ -744,6 +745,7 @@
744
745
  let backgroundSource = '';
745
746
  let backgroundName = '';
746
747
  let activeWorker = null;
748
+ let activeWorkerStartupTimer = null;
747
749
  let renderToken = 0;
748
750
  let syntaxErrorLine = null;
749
751
  let sourceReference = { kind: 'custom' };
@@ -957,9 +959,29 @@
957
959
  runButton.disabled = true;
958
960
  stopButton.disabled = false;
959
961
 
960
- const workerUrl = new URL('./src/playground-worker.js?playground=20260803d', location.href);
962
+ const workerUrl = new URL('./src/playground-worker.js?playground=20260807b', location.href);
961
963
  activeWorker = new Worker(workerUrl, { type: 'module' });
962
- activeWorker.onmessage = (event) => finishRun(event.data);
964
+ const worker = activeWorker;
965
+ const request = {
966
+ source: combinedSource(),
967
+ options: { goals: selectedGoals(), proof: proof.checked, stats: stats.checked },
968
+ };
969
+ activeWorkerStartupTimer = setTimeout(() => {
970
+ if (activeWorker !== worker) return;
971
+ finishRun({
972
+ ok: false,
973
+ error: 'The reasoning worker did not finish loading. Reload the page to refresh cached EyeProlog assets.',
974
+ });
975
+ }, 10000);
976
+ activeWorker.onmessage = (event) => {
977
+ if (event.data?.type === 'ready') {
978
+ clearTimeout(activeWorkerStartupTimer);
979
+ activeWorkerStartupTimer = null;
980
+ worker.postMessage(request);
981
+ return;
982
+ }
983
+ finishRun(event.data);
984
+ };
963
985
  activeWorker.onerror = (event) => {
964
986
  const location = event.filename
965
987
  ? ` (${event.filename}${event.lineno ? `:${event.lineno}${event.colno ? `:${event.colno}` : ''}` : ''})`
@@ -974,10 +996,6 @@
974
996
  ok: false,
975
997
  error: 'The reasoning worker returned an unreadable message.',
976
998
  });
977
- activeWorker.postMessage({
978
- source: combinedSource(),
979
- options: { goals: selectedGoals(), proof: proof.checked, stats: stats.checked },
980
- });
981
999
  }
982
1000
 
983
1001
  function combinedSource() {
@@ -1072,6 +1090,8 @@
1072
1090
  }
1073
1091
 
1074
1092
  function cleanupWorker() {
1093
+ if (activeWorkerStartupTimer != null) clearTimeout(activeWorkerStartupTimer);
1094
+ activeWorkerStartupTimer = null;
1075
1095
  if (activeWorker) activeWorker.terminate();
1076
1096
  activeWorker = null;
1077
1097
  }
package/src/cli.js CHANGED
@@ -115,7 +115,7 @@ async function loadEngine() {
115
115
  import('./program.js'),
116
116
  import('./solver.js'),
117
117
  import('./iso.js'),
118
- import('./eyeprolog-library.js'),
118
+ import('./eyeprolog-autoload.js'),
119
119
  ]);
120
120
  engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library };
121
121
  }
package/src/explain.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // human-readable and machine-readable.
5
5
  import { ATOM, COMPOUND, Env, Term, VAR, deref, flattenConjunction, freshTerm, termToString, unify, variantTerms } from './term.js';
6
6
  import { selectClauseCandidates } from './program.js';
7
- import { getEyePrologRegistry } from './eyeprolog-library.js';
7
+ import { getEyePrologRegistry } from './eyeprolog-autoload.js';
8
8
  import { Solver, nextFreshId } from './solver.js';
9
9
 
10
10
  export function whyProof(program, goal, options = {}) {
@@ -1,4 +1,4 @@
1
- // Load and autoload the pure-Prolog EyeProlog library in Node and the browser.
1
+ // Autoload the pure-Prolog EyeProlog library in Node and the browser.
2
2
  import { createDefaultRegistry } from './iso.js';
3
3
  import { parseClauses } from './parser.js';
4
4
  import { fs, isNode } from './platform.js';
@@ -8,7 +8,7 @@ export const eyePrologNativeLibraryIndicators = Object.freeze([]);
8
8
  export const eyePrologPortableLibraryIndicators = Object.freeze([
9
9
  'uuid/3',
10
10
  'difference/3',
11
- 'call/3',
11
+ 'apply/3',
12
12
  'maplist/3',
13
13
  'tan/2',
14
14
  'asin/2',
@@ -51,7 +51,7 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
51
51
  'min_list/2',
52
52
  'max_list/2',
53
53
  'list_to_set/2',
54
- 'sort/2',
54
+ 'sort_unique/2',
55
55
  'countall/2',
56
56
  'sumall/3',
57
57
  'aggregate_min/5',
@@ -64,12 +64,21 @@ export const eyePrologLibraryIndicators = Object.freeze([
64
64
  ]);
65
65
 
66
66
  const autoloadedPrograms = new WeakSet();
67
- const libraryUrl = new URL('./eyeprolog-library.pl', import.meta.url);
68
- const librarySource = await loadLibrarySource(libraryUrl);
69
- const portableClauseTemplates = parseClauses(librarySource, {
70
- filename: 'src/eyeprolog-library.pl',
67
+ const libraryFiles = [
68
+ 'eyeprolog-library.pl',
69
+ 'eyeprolog-common-library.pl',
70
+ ];
71
+ const libraryCacheKey = isNode
72
+ ? null
73
+ : (new URL(import.meta.url).searchParams.get('playground') ?? '20260807b');
74
+ const librarySources = await Promise.all(libraryFiles.map(async (filename) => ({
75
+ filename,
76
+ source: await loadLibrarySource(libraryFileUrl(filename)),
77
+ })));
78
+ const portableClauseTemplates = librarySources.flatMap(({ filename, source }) => parseClauses(source, {
79
+ filename: `src/${filename}`,
71
80
  sourceMetadata: true,
72
- });
81
+ }));
73
82
 
74
83
  async function loadLibrarySource(url) {
75
84
  if (isNode) return fs.readFileSync(url, 'utf8');
@@ -78,6 +87,12 @@ async function loadLibrarySource(url) {
78
87
  return response.text();
79
88
  }
80
89
 
90
+ function libraryFileUrl(filename) {
91
+ const url = new URL(`./${filename}`, import.meta.url);
92
+ if (!isNode && libraryCacheKey) url.searchParams.set('playground', libraryCacheKey);
93
+ return url;
94
+ }
95
+
81
96
  export function ensureEyePrologLibrary(program) {
82
97
  if (autoloadedPrograms.has(program)) return program;
83
98
 
@@ -0,0 +1,44 @@
1
+ % Common pure-Prolog library predicates for EyeProlog.
2
+ %
3
+ % EyeProlog autoloads this file together with eyeprolog-library.pl. Other
4
+ % Prolog systems should load eyeprolog-library.pl only and use their native or
5
+ % library versions of these widespread predicates, avoiding redefinition of
6
+ % protected procedures.
7
+
8
+ maplist(_, [], []).
9
+ maplist(Closure, [A|As], [B|Bs]) :-
10
+ apply(Closure, A, B),
11
+ maplist(Closure, As, Bs).
12
+
13
+ append([], Ys, Ys).
14
+ append([X|Xs], Ys, [X|Zs]) :- append(Xs, Ys, Zs).
15
+
16
+ member(X, [X|_]).
17
+ member(X, [_|Xs]) :- member(X, Xs).
18
+
19
+ select(X, [X|Xs], Xs).
20
+ select(X, [Y|Ys], [Y|Zs]) :- select(X, Ys, Zs).
21
+
22
+ last([X], X).
23
+ last([_|Xs], X) :- last(Xs, X).
24
+
25
+ nth0(0, [X|_], X).
26
+ nth0(N, [_|Xs], X) :- var(N), nth0(N0, Xs, X), N is N0 + 1.
27
+ nth0(N, [_|Xs], X) :- nonvar(N), N > 0, N1 is N - 1, nth0(N1, Xs, X).
28
+
29
+ nth1(N, List, X) :- nth0(N0, List, X), N is N0 + 1.
30
+
31
+ reverse(List, Reversed) :- eyeprolog__reverse(List, [], Reversed).
32
+
33
+ length(List, Length) :- nonvar(List), eyeprolog__length_count(List, 0, Length).
34
+ length(List, Length) :- var(List), integer(Length), Length >= 0, eyeprolog__length_make(Length, List).
35
+
36
+ sum_list(List, Sum) :- eyeprolog__sum_list(List, 0, Sum).
37
+
38
+ min_list([X|Xs], Min) :- eyeprolog__min_list(Xs, X, Min).
39
+
40
+ max_list([X|Xs], Max) :- eyeprolog__max_list(Xs, X, Max).
41
+
42
+ list_to_set(List, Set) :- eyeprolog__list_to_set(List, [], Set).
43
+
44
+ countall(Goal, Count) :- findall(1, Goal, Ones), eyeprolog__length_count(Ones, 0, Count).
@@ -37,17 +37,18 @@ eyeprolog__atomic_chars(Value, Chars) :-
37
37
 
38
38
  % ---------- core/meta ----------
39
39
 
40
- call(Closure, A, B) :-
40
+ eyeprolog__append([], Ys, Ys).
41
+ eyeprolog__append([X|Xs], Ys, [X|Zs]) :- eyeprolog__append(Xs, Ys, Zs).
42
+
43
+ eyeprolog__member(X, [X|_]).
44
+ eyeprolog__member(X, [_|Xs]) :- eyeprolog__member(X, Xs).
45
+
46
+ apply(Closure, A, B) :-
41
47
  Closure =.. Parts,
42
- append(Parts, [A, B], CallParts),
48
+ eyeprolog__append(Parts, [A, B], CallParts),
43
49
  Goal =.. CallParts,
44
50
  call(Goal).
45
51
 
46
- maplist(_, [], []).
47
- maplist(Closure, [A|As], [B|Bs]) :-
48
- call(Closure, A, B),
49
- maplist(Closure, As, Bs).
50
-
51
52
  % ---------- arithmetic helpers ----------
52
53
 
53
54
  tan(X, Y) :- Y is sin(X) / cos(X).
@@ -86,7 +87,7 @@ eyeprolog__duration_field([C|Cs], [C|Ds], Unit, Rest) :-
86
87
  char_code(C, Code), Code >= 48, Code =< 57,
87
88
  eyeprolog__duration_field(Cs, Ds, Unit, Rest).
88
89
  eyeprolog__duration_field([Unit|Rest], [], Unit, Rest) :-
89
- member(Unit, ['Y', 'M', 'D']).
90
+ eyeprolog__member(Unit, ['Y', 'M', 'D']).
90
91
 
91
92
  eyeprolog__duration_assign('Y', N, 0, M, D, N, M, D).
92
93
  eyeprolog__duration_assign('M', N, Y, 0, D, Y, N, D).
@@ -174,10 +175,10 @@ uuid(Seed0, UUID, Seed) :-
174
175
  eyeprolog__hex_digit(VariantValue, Variant),
175
176
  eyeprolog__uuid_hex(3, Seed4, Group4, Seed5),
176
177
  eyeprolog__uuid_hex(12, Seed5, Group5, Seed),
177
- append(Group1, ['-'|Tail1], Chars),
178
- append(Group2, ['-','4'|Tail2], Tail1),
179
- append(Group3, ['-',Variant|Tail3], Tail2),
180
- append(Group4, ['-'|Group5], Tail3),
178
+ eyeprolog__append(Group1, ['-'|Tail1], Chars),
179
+ eyeprolog__append(Group2, ['-','4'|Tail2], Tail1),
180
+ eyeprolog__append(Group3, ['-',Variant|Tail3], Tail2),
181
+ eyeprolog__append(Group4, ['-'|Group5], Tail3),
181
182
  atom_chars(UUID, Chars).
182
183
 
183
184
  eyeprolog__uuid_hex(0, Seed, [], Seed).
@@ -215,7 +216,7 @@ smallest_divisor_from(N, Start, Divisor) :-
215
216
  % in the exact range covered by bases 2,3,5,7,11,13,17. Above that range the
216
217
  % implementation falls back to exact trial division, preserving semantics for
217
218
  % arbitrary-size integers.
218
- eyeprolog__smallest_divisor_fast(N, Start, N) :-
219
+ eyeprolog__smallest_divisor_fast(N, _, N) :-
219
220
  N >= 2,
220
221
  N < 341550071728321,
221
222
  eyeprolog__mr_prime(N),
@@ -327,8 +328,8 @@ eyeprolog__previous_month(Y, 1, PY, 12) :- PY is Y - 1.
327
328
 
328
329
  eyeprolog__days_in_month(Y, 2, 29) :- eyeprolog__leap_year(Y).
329
330
  eyeprolog__days_in_month(Y, 2, 28) :- \+ eyeprolog__leap_year(Y).
330
- eyeprolog__days_in_month(_, M, 30) :- member(M, [4,6,9,11]).
331
- eyeprolog__days_in_month(_, M, 31) :- member(M, [1,3,5,7,8,10,12]).
331
+ eyeprolog__days_in_month(_, M, 30) :- eyeprolog__member(M, [4,6,9,11]).
332
+ eyeprolog__days_in_month(_, M, 31) :- eyeprolog__member(M, [1,3,5,7,8,10,12]).
332
333
 
333
334
  eyeprolog__leap_year(Y) :- 0 is Y mod 400.
334
335
  eyeprolog__leap_year(Y) :- Y mod 100 =\= 0, 0 is Y mod 4.
@@ -340,15 +341,15 @@ eyeprolog__format_duration(Y, M, D, Duration) :-
340
341
  eyeprolog__duration_part(Y, 'Y', YC),
341
342
  eyeprolog__duration_part(M, 'M', MC),
342
343
  eyeprolog__duration_part(D, 'D', DC),
343
- append(['P'|YC], MC, A),
344
- append(A, DC, Chars),
344
+ eyeprolog__append(['P'|YC], MC, A),
345
+ eyeprolog__append(A, DC, Chars),
345
346
  eyeprolog__text_chars(Duration, Chars).
346
347
 
347
348
  eyeprolog__duration_part(0, _, []).
348
349
  eyeprolog__duration_part(N, Unit, Chars) :-
349
350
  N =\= 0,
350
351
  number_chars(N, Digits),
351
- append(Digits, [Unit], Chars).
352
+ eyeprolog__append(Digits, [Unit], Chars).
352
353
 
353
354
  % ---------- portable named-capture matcher ----------
354
355
  %
@@ -383,8 +384,8 @@ eyeprolog__regex_parse(Chars0, Start, End, Tokens) :-
383
384
  eyeprolog__strip_start_anchor(['^'|Cs], yes, Cs).
384
385
  eyeprolog__strip_start_anchor(Cs, no, Cs).
385
386
 
386
- eyeprolog__strip_end_anchor(Cs0, yes, Cs) :- append(Cs, ['$'], Cs0).
387
- eyeprolog__strip_end_anchor(Cs, no, Cs) :- \+ append(_, ['$'], Cs).
387
+ eyeprolog__strip_end_anchor(Cs0, yes, Cs) :- eyeprolog__append(Cs, ['$'], Cs0).
388
+ eyeprolog__strip_end_anchor(Cs, no, Cs) :- \+ eyeprolog__append(_, ['$'], Cs).
388
389
 
389
390
  eyeprolog__regex_tokens_parse([], []).
390
391
  eyeprolog__regex_tokens_parse(['('|Cs], [capture(Name, Kind, Optional)|Tokens]) :-
@@ -425,12 +426,12 @@ eyeprolog__capture_kind(['['|Body], class_exact(Class, Count)) :-
425
426
  Class \= [].
426
427
  eyeprolog__capture_kind(Body, literal(Body)) :-
427
428
  Body \= [],
428
- \+ member('(', Body),
429
- \+ member(')', Body),
430
- \+ member('[', Body),
431
- \+ member(']', Body),
432
- \+ member('+', Body),
433
- \+ member('*', Body).
429
+ \+ eyeprolog__member('(', Body),
430
+ \+ eyeprolog__member(')', Body),
431
+ \+ eyeprolog__member('[', Body),
432
+ \+ eyeprolog__member(']', Body),
433
+ \+ eyeprolog__member('+', Body),
434
+ \+ eyeprolog__member('*', Body).
434
435
 
435
436
  eyeprolog__has_capture([capture(_,_,_)|_]).
436
437
  eyeprolog__has_capture([_|Ts]) :- eyeprolog__has_capture(Ts).
@@ -449,7 +450,7 @@ eyeprolog__regex_tokens([capture(Name,Kind,yes)|Ts], Chars, Rest, [capture(Name,
449
450
  eyeprolog__regex_tokens([capture(_,_,yes)|Ts], Chars, Rest, Captures) :-
450
451
  eyeprolog__regex_tokens(Ts, Chars, Rest, Captures).
451
452
 
452
- eyeprolog__capture_match(literal(Literal), Chars, Rest, Literal) :- append(Literal, Rest, Chars).
453
+ eyeprolog__capture_match(literal(Literal), Chars, Rest, Literal) :- eyeprolog__append(Literal, Rest, Chars).
453
454
  eyeprolog__capture_match(word_plus, Chars, Rest, Value) :- eyeprolog__take_class_plus(word, Chars, Value, Rest).
454
455
  eyeprolog__capture_match(nonspace_plus, Chars, Rest, Value) :- eyeprolog__take_class_plus(nonspace, Chars, Value, Rest).
455
456
  eyeprolog__capture_match(class_plus(Class), Chars, Rest, Value) :- eyeprolog__take_class_plus(class(Class), Chars, Value, Rest).
@@ -494,33 +495,30 @@ eyeprolog__captures_context([capture(Name,Value)|Rest], (Term,Context)) :-
494
495
 
495
496
  % ---------- text/list processing ----------
496
497
 
497
- append([], Ys, Ys).
498
- append([X|Xs], Ys, [X|Zs]) :- append(Xs, Ys, Zs).
499
-
500
498
  string_concat(A, B, Whole) :-
501
499
  nonvar(A), nonvar(B),
502
500
  !,
503
501
  eyeprolog__text_chars(A, AC),
504
502
  eyeprolog__text_chars(B, BC),
505
- append(AC, BC, WC),
503
+ eyeprolog__append(AC, BC, WC),
506
504
  eyeprolog__text_chars(Whole, WC).
507
505
  string_concat(A, B, Whole) :-
508
506
  nonvar(Whole),
509
507
  eyeprolog__text_chars(Whole, WC),
510
- append(AC, BC, WC),
508
+ eyeprolog__append(AC, BC, WC),
511
509
  eyeprolog__text_chars(A, AC),
512
510
  eyeprolog__text_chars(B, BC).
513
511
 
514
512
  contains(Text, Needle) :-
515
513
  eyeprolog__text_chars(Text, TextChars),
516
514
  eyeprolog__text_chars(Needle, NeedleChars),
517
- append(_, Tail, TextChars),
518
- append(NeedleChars, _, Tail),
515
+ eyeprolog__append(_, Tail, TextChars),
516
+ eyeprolog__append(NeedleChars, _, Tail),
519
517
  !.
520
518
 
521
519
  matches(Text, Pattern) :-
522
520
  split(Pattern, '|', Alternatives),
523
- member(Needle, Alternatives),
521
+ eyeprolog__member(Needle, Alternatives),
524
522
  contains(Text, Needle),
525
523
  !.
526
524
 
@@ -533,8 +531,8 @@ split(Text, Separator, Parts) :-
533
531
  eyeprolog__split_chars(Chars, [], Parts) :- eyeprolog__split_each_char(Chars, Parts).
534
532
  eyeprolog__split_chars(Chars, Separator, [Prefix|Parts]) :-
535
533
  Separator \= [],
536
- append(Prefix, Tail, Chars),
537
- append(Separator, Rest, Tail),
534
+ eyeprolog__append(Prefix, Tail, Chars),
535
+ eyeprolog__append(Separator, Rest, Tail),
538
536
  !,
539
537
  eyeprolog__split_chars(Rest, Separator, Parts).
540
538
  eyeprolog__split_chars(Chars, Separator, [Chars]) :- Separator \= [].
@@ -586,9 +584,9 @@ eyeprolog__upper_code(Code, Code) :- (Code < 97 ; Code > 122).
586
584
  trim(Text, Trimmed) :-
587
585
  eyeprolog__text_chars(Text, Chars),
588
586
  eyeprolog__drop_space(Chars, Left),
589
- reverse(Left, Reversed),
587
+ eyeprolog__reverse(Left, [], Reversed),
590
588
  eyeprolog__drop_space(Reversed, RightReversed),
591
- reverse(RightReversed, TrimmedChars),
589
+ eyeprolog__reverse(RightReversed, [], TrimmedChars),
592
590
  eyeprolog__text_chars(Trimmed, TrimmedChars).
593
591
 
594
592
  eyeprolog__drop_space([C|Cs], Out) :-
@@ -632,15 +630,15 @@ eyeprolog__term_chars(Term, Chars) :- atom(Term), atom_chars(Term, Chars).
632
630
  eyeprolog__term_chars([], ['[',']']).
633
631
  eyeprolog__term_chars([H|T], Chars) :-
634
632
  eyeprolog__list_term_chars([H|T], Body),
635
- append(['['|Body], [']'], Chars).
633
+ eyeprolog__append(['['|Body], [']'], Chars).
636
634
  eyeprolog__term_chars(Term, Chars) :-
637
635
  compound(Term),
638
636
  Term \= [_|_],
639
637
  Term =.. [Name|Args],
640
638
  atom_chars(Name, NameChars),
641
639
  eyeprolog__term_args_chars(Args, ArgsChars),
642
- append(NameChars, ['('|ArgsChars], A),
643
- append(A, [')'], Chars).
640
+ eyeprolog__append(NameChars, ['('|ArgsChars], A),
641
+ eyeprolog__append(A, [')'], Chars).
644
642
 
645
643
  eyeprolog__list_term_chars([H], Chars) :-
646
644
  eyeprolog__term_chars(H, Chars).
@@ -648,19 +646,19 @@ eyeprolog__list_term_chars([H|T], Chars) :-
648
646
  T = [_|_],
649
647
  eyeprolog__term_chars(H, HC),
650
648
  eyeprolog__list_term_chars(T, TC),
651
- append(HC, [',',' '|TC], Chars).
649
+ eyeprolog__append(HC, [',',' '|TC], Chars).
652
650
  eyeprolog__list_term_chars([H|T], Chars) :-
653
651
  T \= [], T \= [_|_],
654
652
  eyeprolog__term_chars(H, HC),
655
653
  eyeprolog__term_chars(T, TC),
656
- append(HC, [' ','|',' '|TC], Chars).
654
+ eyeprolog__append(HC, [' ','|',' '|TC], Chars).
657
655
 
658
656
  eyeprolog__term_args_chars([A], Chars) :- eyeprolog__term_chars(A, Chars).
659
657
  eyeprolog__term_args_chars([A|As], Chars) :-
660
658
  As \= [],
661
659
  eyeprolog__term_chars(A, AC),
662
660
  eyeprolog__term_args_chars(As, Rest),
663
- append(AC, [',',' '|Rest], Chars).
661
+ eyeprolog__append(AC, [',',' '|Rest], Chars).
664
662
 
665
663
  join([], _, Out) :- eyeprolog__text_chars(Out, []).
666
664
  join([Item|Items], Separator, Out) :-
@@ -672,8 +670,8 @@ join([Item|Items], Separator, Out) :-
672
670
  eyeprolog__join_chars([], _, Chars, Chars).
673
671
  eyeprolog__join_chars([Item|Items], Separator, Prefix, Out) :-
674
672
  eyeprolog__atomic_chars(Item, ItemChars),
675
- append(Prefix, Separator, A),
676
- append(A, ItemChars, B),
673
+ eyeprolog__append(Prefix, Separator, A),
674
+ eyeprolog__append(A, ItemChars, B),
677
675
  eyeprolog__join_chars(Items, Separator, B, Out).
678
676
 
679
677
  substring(Text, Start, Count, Out) :-
@@ -685,21 +683,6 @@ substring(Text, Start, Count, Out) :-
685
683
 
686
684
  % ---------- list relations ----------
687
685
 
688
- member(X, [X|_]).
689
- member(X, [_|Xs]) :- member(X, Xs).
690
-
691
- select(X, [X|Xs], Xs).
692
- select(X, [Y|Ys], [Y|Zs]) :- select(X, Ys, Zs).
693
-
694
- last([X], X).
695
- last([_|Xs], X) :- last(Xs, X).
696
-
697
- nth0(0, [X|_], X).
698
- nth0(N, [_|Xs], X) :- var(N), nth0(N0, Xs, X), N is N0 + 1.
699
- nth0(N, [_|Xs], X) :- nonvar(N), N > 0, N1 is N - 1, nth0(N1, Xs, X).
700
-
701
- nth1(N, List, X) :- nth0(N0, List, X), N is N0 + 1.
702
-
703
686
  set_nth0(0, [_|Xs], X, [X|Xs]).
704
687
  set_nth0(N, [Y|Ys], X, [Y|Zs]) :- N > 0, N1 is N - 1, set_nth0(N1, Ys, X, Zs).
705
688
 
@@ -711,33 +694,25 @@ drop(N, [_|Xs], Ys) :- N > 0, N1 is N - 1, drop(N1, Xs, Ys).
711
694
 
712
695
  slice(Start, Count, List, Slice) :- drop(Start, List, Tail), take(Count, Tail, Slice).
713
696
 
714
- reverse(List, Reversed) :- eyeprolog__reverse(List, [], Reversed).
715
697
  eyeprolog__reverse([], Acc, Acc).
716
698
  eyeprolog__reverse([X|Xs], Acc, Out) :- eyeprolog__reverse(Xs, [X|Acc], Out).
717
699
 
718
- length(List, Length) :- nonvar(List), eyeprolog__length_count(List, 0, Length).
719
- length(List, Length) :- var(List), integer(Length), Length >= 0, eyeprolog__length_make(Length, List).
720
-
721
700
  eyeprolog__length_count([], N, N).
722
701
  eyeprolog__length_count([_|Xs], N0, N) :- N1 is N0 + 1, eyeprolog__length_count(Xs, N1, N).
723
702
  eyeprolog__length_make(0, []).
724
703
  eyeprolog__length_make(N, [_|Xs]) :- N > 0, N1 is N - 1, eyeprolog__length_make(N1, Xs).
725
704
 
726
- sum_list(List, Sum) :- eyeprolog__sum_list(List, 0, Sum).
727
705
  eyeprolog__sum_list([], Sum, Sum).
728
706
  eyeprolog__sum_list([X|Xs], Acc, Sum) :- Next is Acc + X, eyeprolog__sum_list(Xs, Next, Sum).
729
707
 
730
- min_list([X|Xs], Min) :- eyeprolog__min_list(Xs, X, Min).
731
708
  eyeprolog__min_list([], Min, Min).
732
709
  eyeprolog__min_list([X|Xs], Current, Min) :- X @< Current, eyeprolog__min_list(Xs, X, Min).
733
710
  eyeprolog__min_list([X|Xs], Current, Min) :- X @>= Current, eyeprolog__min_list(Xs, Current, Min).
734
711
 
735
- max_list([X|Xs], Max) :- eyeprolog__max_list(Xs, X, Max).
736
712
  eyeprolog__max_list([], Max, Max).
737
713
  eyeprolog__max_list([X|Xs], Current, Max) :- X @> Current, eyeprolog__max_list(Xs, X, Max).
738
714
  eyeprolog__max_list([X|Xs], Current, Max) :- X @=< Current, eyeprolog__max_list(Xs, Current, Max).
739
715
 
740
- list_to_set(List, Set) :- eyeprolog__list_to_set(List, [], Set).
741
716
  eyeprolog__list_to_set([], _, []).
742
717
  eyeprolog__list_to_set([X|Xs], Seen, Set) :-
743
718
  eyeprolog__identical_member(X, Seen),
@@ -749,7 +724,7 @@ eyeprolog__list_to_set([X|Xs], Seen, [X|Set]) :-
749
724
  eyeprolog__identical_member(X, [Y|_]) :- X == Y.
750
725
  eyeprolog__identical_member(X, [_|Ys]) :- eyeprolog__identical_member(X, Ys).
751
726
 
752
- sort(List, Sorted) :- eyeprolog__sort(List, [], Sorted).
727
+ sort_unique(List, Sorted) :- eyeprolog__sort(List, [], Sorted).
753
728
  eyeprolog__sort([], Sorted, Sorted).
754
729
  eyeprolog__sort([X|Xs], Acc, Sorted) :-
755
730
  eyeprolog__insert_sorted(X, Acc, Next),
@@ -762,11 +737,9 @@ eyeprolog__insert_sorted(X, [Y|Ys], [Y|Zs]) :- X @> Y, eyeprolog__insert_sorted(
762
737
 
763
738
  % ---------- aggregation ----------
764
739
 
765
- countall(Goal, Count) :- findall(1, Goal, Ones), length(Ones, Count).
766
-
767
740
  sumall(Expression, Goal, Sum) :-
768
741
  findall(Value, (Goal, Value is Expression), Values),
769
- sum_list(Values, Sum).
742
+ eyeprolog__sum_list(Values, 0, Sum).
770
743
 
771
744
  aggregate_min(Key, Value, Goal, BestKey, BestValue) :-
772
745
  findall(pair(Key, Value), Goal, Pairs),
@@ -775,7 +748,7 @@ aggregate_min(Key, Value, Goal, BestKey, BestValue) :-
775
748
  eyeprolog__aggregate_min([pair(K,V)|Pairs], BestKey, BestValue) :-
776
749
  eyeprolog__aggregate_min_rest(Pairs, K, V, BestKey, BestValue).
777
750
  eyeprolog__aggregate_min_rest([], K, V, K, V).
778
- eyeprolog__aggregate_min_rest([pair(K,V)|Pairs], CK, CV, BK, BV) :-
751
+ eyeprolog__aggregate_min_rest([pair(K,V)|Pairs], CK, _, BK, BV) :-
779
752
  K @< CK,
780
753
  eyeprolog__aggregate_min_rest(Pairs, K, V, BK, BV).
781
754
  eyeprolog__aggregate_min_rest([pair(K,_)|Pairs], CK, CV, BK, BV) :-
@@ -789,7 +762,7 @@ aggregate_max(Key, Value, Goal, BestKey, BestValue) :-
789
762
  eyeprolog__aggregate_max([pair(K,V)|Pairs], BestKey, BestValue) :-
790
763
  eyeprolog__aggregate_max_rest(Pairs, K, V, BestKey, BestValue).
791
764
  eyeprolog__aggregate_max_rest([], K, V, K, V).
792
- eyeprolog__aggregate_max_rest([pair(K,V)|Pairs], CK, CV, BK, BV) :-
765
+ eyeprolog__aggregate_max_rest([pair(K,V)|Pairs], CK, _, BK, BV) :-
793
766
  K @> CK,
794
767
  eyeprolog__aggregate_max_rest(Pairs, K, V, BK, BV).
795
768
  eyeprolog__aggregate_max_rest([pair(K,_)|Pairs], CK, CV, BK, BV) :-
package/src/index.js CHANGED
@@ -18,7 +18,7 @@ export {
18
18
  eyePrologLibraryIndicators,
19
19
  eyePrologNativeLibraryIndicators,
20
20
  eyePrologPortableLibraryIndicators,
21
- } from './eyeprolog-library.js';
21
+ } from './eyeprolog-autoload.js';
22
22
  export { StreamManager } from './io.js';
23
23
 
24
24
  import { ATOM, COMPOUND, VAR, Env, copyResolved, termIsGround, termToString } from './term.js';
@@ -26,7 +26,7 @@ import { Program } from './program.js';
26
26
  import { Solver } from './solver.js';
27
27
  import { whyNoProof, whyProof } from './explain.js';
28
28
  import { HaltSignal, PrologError } from './iso.js';
29
- import { getEyePrologRegistry } from './eyeprolog-library.js';
29
+ import { getEyePrologRegistry } from './eyeprolog-autoload.js';
30
30
  import { parseGoalText } from './parser.js';
31
31
 
32
32
  export function run(source, options = {}) {
@@ -1,8 +1,7 @@
1
1
  // Browser worker entry used by playground.html.
2
2
  // Keep this module free of Node-only imports: it is fetched directly by the
3
3
  // browser and is also exercised by test/run-playground.mjs.
4
- import { run } from './index.js?playground=20260803c';
5
- import { createEyePrologRegistry } from './eyeprolog-library.js?playground=20260803c';
4
+ import { createEyePrologRegistry, run } from './index.js?playground=20260807b';
6
5
 
7
6
  const registry = createEyePrologRegistry();
8
7
 
@@ -42,4 +41,5 @@ function defaultNow() {
42
41
 
43
42
  if (typeof self !== 'undefined' && typeof self.postMessage === 'function') {
44
43
  installPlaygroundWorker(self);
44
+ self.postMessage({ type: 'ready' });
45
45
  }
package/src/solver.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variantTerms,
6
6
  } from './term.js';
7
7
  import { PrologError } from './iso.js';
8
- import { ensureEyePrologLibrary, getEyePrologRegistry } from './eyeprolog-library.js';
8
+ import { ensureEyePrologLibrary, getEyePrologRegistry } from './eyeprolog-autoload.js';
9
9
  import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
10
10
  import { StreamManager } from './io.js';
11
11
 
@@ -6,6 +6,5 @@ answer(set_nth0, X) :- set_nth0(1, [a, b, c], x, X).
6
6
  answer(reverse, X) :- reverse([a, b, c], X).
7
7
  answer(length, N) :- length([a, b, c], N).
8
8
  answer(findall, X) :- findall(N, between(1, 3, N), X).
9
- answer(sort, X) :- sort([b, a, b], X).
9
+ answer(sort, X) :- sort_unique([b, a, b], X).
10
10
  %% goal: answer(X0, X1)
11
-
@@ -1,4 +1,3 @@
1
- % Reference 9.8: sort/2 sorts and deduplicates a proper list.
2
- answer(sorted, X) :- sort([c, a, b, a], X).
1
+ % Reference 9.8: sort_unique/2 sorts and deduplicates a proper list.
2
+ answer(sorted, X) :- sort_unique([c, a, b, a], X).
3
3
  %% goal: answer(X0, X1)
4
-
@@ -4,6 +4,6 @@
4
4
  answer(length_empty, X) :- length([], X).
5
5
  answer(length_nested, X) :- length([[a], [b, c], []], X).
6
6
  answer(reverse_atoms, X) :- reverse([a, b, c], X).
7
- answer(sort_numbers, X) :- sort([3, 1, 2, 1], X).
8
- answer(sort_mixed_terms, X) :- sort([b, 2, a, 1, pair(a), "s"], X).
7
+ answer(sort_numbers, X) :- sort_unique([3, 1, 2, 1], X).
8
+ answer(sort_mixed_terms, X) :- sort_unique([b, 2, a, 1, pair(a), "s"], X).
9
9
  answer(reverse_empty, X) :- reverse([], X).
@@ -1,3 +1,3 @@
1
1
  %% goal: answer(X0, X1)
2
2
 
3
- answer(sort_iri_atoms, X) :- sort(['<urn:example:b>', '<urn:example:a>'], X).
3
+ answer(sort_iri_atoms, X) :- sort_unique(['<urn:example:b>', '<urn:example:a>'], X).
@@ -1,3 +1,3 @@
1
1
  %% goal: answer(X0)
2
2
 
3
- answer(X) :- sort([b(2), a(3), a(1)], X).
3
+ answer(X) :- sort_unique([b(2), a(3), a(1)], X).
@@ -22,6 +22,8 @@ export async function runPlayground(reporter = new TestReporter()) {
22
22
  const html = fs.readFileSync(path.join(packageRoot, 'playground.html'), 'utf8');
23
23
  assertIncludes(html, "new URL('./src/playground-worker.js?playground=", 'playground worker URL');
24
24
  assertIncludes(html, "new Worker(workerUrl, { type: 'module' })", 'module worker construction');
25
+ assertIncludes(html, "event.data?.type === 'ready'", 'worker readiness handshake');
26
+ assertIncludes(html, 'did not finish loading', 'worker startup timeout');
25
27
  assertNotIncludes(html, 'URL.createObjectURL(new Blob([workerCode]', 'inline blob worker');
26
28
  });
27
29
 
@@ -80,6 +82,8 @@ export async function runPlayground(reporter = new TestReporter()) {
80
82
  ['playground.html', 'text/html'],
81
83
  ['src/playground-worker.js', 'text/javascript'],
82
84
  ['src/index.js', 'text/javascript'],
85
+ ['src/eyeprolog-library.pl', 'text/plain'],
86
+ ['src/eyeprolog-common-library.pl', 'text/plain'],
83
87
  ['examples/socrates.pl', 'text/plain'],
84
88
  ];
85
89
  for (const [relative, contentType] of expected) {
@@ -94,7 +98,7 @@ export async function runPlayground(reporter = new TestReporter()) {
94
98
  await withStaticServer(async (baseUrl) => {
95
99
  const modules = await crawlModuleGraph(new URL('src/playground-worker.js?playground=test', baseUrl));
96
100
  assert(modules.size >= 10, `expected a substantial worker module graph, got ${modules.size}`);
97
- assert([...modules].some((url) => url.includes('/src/eyeprolog-library.js')), 'EyeProlog library missing from worker graph');
101
+ assert([...modules].some((url) => url.includes('/src/eyeprolog-autoload.js')), 'EyeProlog autoloader missing from worker graph');
98
102
  assert([...modules].some((url) => url.includes('/src/solver.js')), 'solver missing from worker graph');
99
103
  });
100
104
  });
@@ -584,7 +584,7 @@ function documentationSyncCases() {
584
584
  '[Book — *The Art of EyeProlog*](https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog)',
585
585
  'README links to the book',
586
586
  );
587
- for (const filename of ['src/iso.js', 'src/eyeprolog-library.js', 'src/playground-worker.js']) {
587
+ for (const filename of ['src/iso.js', 'src/eyeprolog-autoload.js', 'src/playground-worker.js']) {
588
588
  assertEqual(fs.existsSync(path.join(packageRoot, filename)), true, `${filename} exists`);
589
589
  assertIncludes(book, filename, `book documents ${filename}`);
590
590
  }
@@ -1047,7 +1047,8 @@ open(X) :- candidate(X), \\+ closed(X).
1047
1047
  assertEqual(betweenGenerator.cutRecursive, true, 'portable between generator has deterministic recursive control');
1048
1048
  assertEqual(betweenGenerator.tabled, false, 'portable between generator avoids suffix answer tables');
1049
1049
  assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'eyeprolog-library.pl')), true, 'portable Prolog source exists');
1050
- assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'eyeprolog-library.js')), true, 'portable source loader exists');
1050
+ assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'eyeprolog-autoload.js')), true, 'portable source autoloader exists');
1051
+ assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'eyeprolog-common-library.pl')), true, 'pure-Prolog common library exists');
1051
1052
  assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'library-source.js')), false, 'duplicate source loader is absent');
1052
1053
  assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'portable-library.js')), false, 'obsolete duplicate module remains absent');
1053
1054
  assertEqual(run(program, { goal: 'answer(X)' }).stdout, 'answer([a, b]).\n', 'autoloaded append execution');
@@ -1058,7 +1059,9 @@ open(X) :- candidate(X), \\+ closed(X).
1058
1059
  name: 'portable library executes against the ISO-only registry',
1059
1060
  run: () => {
1060
1061
  const portableSource = fs.readFileSync(path.join(packageRoot, 'src', 'eyeprolog-library.pl'), 'utf8');
1062
+ const commonSource = fs.readFileSync(path.join(packageRoot, 'src', 'eyeprolog-common-library.pl'), 'utf8');
1061
1063
  const program = Program.parse(`${portableSource}
1064
+ ${commonSource}
1062
1065
  portable_check(A, B, C) :- lowercase('HELLO', A), replace('banana', 'na', 'NA', B), append([x], [y], C).
1063
1066
  `);
1064
1067
  const solver = new Solver(program, { registry: createDefaultRegistry() });
@@ -1712,7 +1715,7 @@ function playgroundStaticIssues() {
1712
1715
  if (!html.includes("new URL('./src/playground-worker.js?playground=")) issues.push('playground must cache-bust its dedicated module worker');
1713
1716
  if (!html.includes("new Worker(workerUrl, { type: 'module' })")) issues.push('playground must launch the dedicated module worker');
1714
1717
  const workerText = fs.readFileSync(path.join(packageRoot, 'src', 'playground-worker.js'), 'utf8');
1715
- if (!workerText.includes("from './eyeprolog-library.js?playground=") ||
1718
+ if (!workerText.includes("from './index.js?playground=") ||
1716
1719
  !workerText.includes('createEyePrologRegistry') ||
1717
1720
  !workerText.includes('executePlaygroundRequest')) {
1718
1721
  issues.push('playground worker must install the EyeProlog library registry');
@@ -907,7 +907,7 @@ reverse_go([X | Xs], Acc, Reversed) :-
907
907
 
908
908
  No mutation occurs; every call receives a new term. EyeProlog also includes
909
909
  `member/2`, `append/3`, `select/3`, `nth0/3`, `reverse/2`, `length/2`,
910
- `sort/2`, slicing helpers, and numeric summaries. Improper lists such as
910
+ `sort_unique/2`, slicing helpers, and numeric summaries. Improper lists such as
911
911
  `[a | Tail]` are valid terms, but operations requiring a proper finite list
912
912
  fail unless the tail is `[]`.
913
913
 
@@ -1109,7 +1109,7 @@ Choose a collector from the question, not from convenience:
1109
1109
  Counting solutions is not necessarily counting distinct domain objects: two
1110
1110
  proofs may resolve the visible value in the same way. When identity matters,
1111
1111
  collect the identifying template and deliberately canonicalize it with
1112
- `sort/2`; when derivation multiplicity matters, retain the duplicates. Making
1112
+ `sort_unique/2`; when derivation multiplicity matters, retain the duplicates. Making
1113
1113
  that decision explicit prevents a database-style summary from silently
1114
1114
  changing the question.
1115
1115
 
@@ -1709,11 +1709,12 @@ truncate search; it does not prove that no further answer exists.
1709
1709
  ### Implementation boundary
1710
1710
 
1711
1711
  The source layout mirrors the language boundary. `src/iso.js` contains the
1712
- isolated ISO processor predicates and registry. `src/eyeprolog-library.pl` is
1713
- the portable EyeProlog library: 50 public predicates written as ordinary
1714
- Prolog clauses against that ISO profile. Its paired
1715
- `src/eyeprolog-library.js` module loads the Prolog file in Node and the browser
1716
- and owns the small autoload integration layer.
1712
+ isolated ISO processor predicates and registry. `src/eyeprolog-library.pl`
1713
+ contains collision-free, self-contained portable extensions.
1714
+ `src/eyeprolog-common-library.pl` supplies common list and aggregation
1715
+ predicates that Trealla, Scryer, and many other systems already provide. The
1716
+ paired `src/eyeprolog-autoload.js` module autoloads both pure-Prolog files in
1717
+ Node and the browser.
1717
1718
  The RDF tools emit IRI and literal lexical values as ISO atoms, matching the
1718
1719
  portable text API without a host representation adapter.
1719
1720
 
@@ -5254,14 +5255,17 @@ so side effects occur in Prolog execution order.
5254
5255
 
5255
5256
  EyeProlog exposes **50 library predicate indicators** in addition to the 115
5256
5257
  indicators in its isolated ISO profile. **All 50 are ordinary Prolog clauses**
5257
- in `src/eyeprolog-library.pl`; none is a native host predicate. The resulting
5258
+ across `src/eyeprolog-library.pl` and `src/eyeprolog-common-library.pl`; none
5259
+ is a native host predicate. The resulting
5258
5260
  normal EyeProlog language surface is therefore **165 public predicate
5259
5261
  indicators**. Internally, the runtime registry contains only the 115 ISO
5260
5262
  definitions; the EyeProlog relations are autoloaded source clauses.
5261
5263
 
5262
- The portable file is autoloaded once into every `Program` used with the
5263
- EyeProlog registry. `src/eyeprolog-library.js` loads it from the package in Node
5264
- or through `fetch()` in the browser and performs the autoload.
5264
+ The two Prolog files are autoloaded once into every `Program` used with the
5265
+ EyeProlog registry. `src/eyeprolog-autoload.js` loads them from the package in
5266
+ Node or through `fetch()` in the browser and performs the autoload. External
5267
+ Prolog systems load only `src/eyeprolog-library.pl`; this avoids redefining
5268
+ their protected or preloaded list predicates.
5265
5269
  The isolated ISO-only registry remains
5266
5270
  available through `createDefaultRegistry()` and `getDefaultRegistry()` for
5267
5271
  conformance work and advanced embedders. Source clauses sharing a portable
@@ -5276,18 +5280,18 @@ standard fallback relation.
5276
5280
  | `nth0/3`, `nth1/3`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` |
5277
5281
  | `reverse/2`, `length/2`, `sum_list/2` |
5278
5282
  | `min_list/2`, `max_list/2` |
5279
- | `list_to_set/2`, `sort/2` |
5283
+ | `list_to_set/2`, `sort_unique/2` |
5280
5284
  | `string_concat/3`, `contains/2`, `matches/2` |
5281
5285
  | `join/3`, `substring/4` |
5282
5286
  | `countall/2`, `sumall/3` |
5283
5287
  | `aggregate_min/5`, `aggregate_max/5` |
5284
5288
  | `between/3`, `smallest_divisor_from/3`, `random/3` |
5289
+ | `apply/3` |
5285
5290
  | `maplist/3` |
5286
5291
  | `acos/2`, `asin/2`, `atan2/3`, `tan/2` |
5287
5292
  | `lt/2`, `le/2`, `gt/2`, `ge/2` |
5288
5293
  | `uuid/3`, `difference/3` |
5289
5294
  | `matches/3` |
5290
- | `call/3` |
5291
5295
  | `split/3`, `replace/4` |
5292
5296
  | `lowercase/2`, `uppercase/2`, `trim/2` |
5293
5297
  | `number_string/2`, `atom_string/2`, `term_string/2` |
@@ -5374,7 +5378,8 @@ zero-based, nonnegative safe integers.
5374
5378
  | `\+ member(+Item,+List)` | Succeeds only when `Item` does not unify with any member. Use it after binding the item and list. |
5375
5379
  | `nth0(?Index,+List,?Item)` | Checks a bound zero-based index or enumerates indexes and their items. |
5376
5380
  | `nth1(?Index,+List,?Item)` | Checks a bound one-based index or enumerates one-based indexes and items. |
5377
- | `maplist(+Closure,+List1,?List2)` | Applies a two-argument closure pairwise; `call/3` supplies the closure arguments and supports partially applied compound closures. |
5381
+ | `apply(+Closure,+A,+B)` | Extends a closure with two arguments using ISO `=../2`, then invokes it through ISO `call/1`. |
5382
+ | `maplist(+Closure,+List1,?List2)` | Applies a two-argument closure pairwise through `apply/3`; partially applied compound closures are supported. |
5378
5383
  | `[Head|Tail] = List` | Decomposes a nonempty list directly with ISO unification; no library wrapper is needed. |
5379
5384
  | `set_nth0(+Index,+List,+Item,-NewList)` | Replaces one existing position without mutating the input list. |
5380
5385
  | `last(+List,?Last)` | Returns the final element of a nonempty proper list. |
@@ -5385,7 +5390,7 @@ zero-based, nonnegative safe integers.
5385
5390
  | `sum_list(+List,-Sum)` | Sums numeric elements with ISO `is/2`. The empty sum is `0`; invalid arithmetic raises the corresponding ISO error. |
5386
5391
  | `min_list(+List,-Min)`, `max_list(+List,-Max)` | Select by EyeProlog term order, not numeric coercion. Empty lists fail. |
5387
5392
  | `list_to_set(+List,-Set)` | Removes later structural duplicates while preserving first-occurrence order. |
5388
- | `sort(+List,-Set)` | Sorts by standard term order and removes structural duplicates. |
5393
+ | `sort_unique(+List,-Set)` | Sorts by standard term order and removes structural duplicates without colliding with engines that protect `sort/2`. |
5389
5394
 
5390
5395
  ```eyeprolog
5391
5396
  answer(split, pair(Prefix, Suffix)) :-
@@ -5739,7 +5744,7 @@ mode at a time.
5739
5744
 
5740
5745
  | Program | Standard facility | Checked answer |
5741
5746
  | --- | --- | --- |
5742
- | [Combinatorics Findall Sort](https://github.com/eyereasoner/eyeprolog/blob/main/examples/combinatorics-findall-sort.pl) | Eyelet-inspired combinations example using findall/3 and sort/2. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/combinatorics-findall-sort.pl) |
5747
+ | [Combinatorics Findall Sort](https://github.com/eyereasoner/eyeprolog/blob/main/examples/combinatorics-findall-sort.pl) | Eyelet-inspired combinations example using findall/3 and `sort_unique/2`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/combinatorics-findall-sort.pl) |
5743
5748
  | [Floating Point](https://github.com/eyereasoner/eyeprolog/blob/main/examples/floating-point.pl) | Floating-point arithmetic and comparisons. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/floating-point.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/floating-point.pl) |
5744
5749
  | [Atomic conversion](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-atomic-conversion.pl) | Atom splitting, character atoms, Unicode codes, and numeric parsing. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-atomic-conversion.pl) |
5745
5750
  | [Control and errors](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-control-and-errors.pl) | `call/1`, `once/1`, cut, if-then-else, `throw/1`, and `catch/3`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-control-and-errors.pl) |
@@ -5817,7 +5822,7 @@ finite search space, and which constraint removes which branches?
5817
5822
 
5818
5823
  | Program | Search design | Checked answer |
5819
5824
  | --- | --- | --- |
5820
- | [Dijkstra Findall Sort](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dijkstra-findall-sort.pl) | Eyelet-inspired Dijkstra example using findall/3 and sort/2. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dijkstra-findall-sort.pl) |
5825
+ | [Dijkstra Findall Sort](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dijkstra-findall-sort.pl) | Eyelet-inspired Dijkstra example using findall/3 and `sort_unique/2`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dijkstra-findall-sort.pl) |
5821
5826
  | [Dijkstra](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dijkstra.pl) | Weighted path enumeration adapted from Eyeling dijkstra.n3. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dijkstra.pl) |
5822
5827
  | [DONALD + GERALD = ROBERT](https://github.com/eyereasoner/eyeprolog/blob/main/examples/donald-gerald-robert.pl) | All ten decimal digits are assigned to ten distinct letters. Right-to-left carry propagation cuts a naive 10! search space to one solution. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/donald-gerald-robert.pl) |
5823
5828
  | [Enigma1225](https://github.com/eyereasoner/eyeprolog/blob/main/examples/enigma1225.pl) | New Scientist Enigma 1225, retaining the best board in one pass with `aggregate_max/5`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/enigma1225.pl) |