eyeprolog 0.5.11 → 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 (43) hide show
  1. package/README.md +5 -3
  2. package/examples/age.pl +7 -7
  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 +6 -6
  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/examples/uuid.pl +4 -4
  22. package/package.json +1 -1
  23. package/playground.html +28 -8
  24. package/src/cli.js +1 -1
  25. package/src/explain.js +1 -1
  26. package/src/eyeprolog-autoload.js +133 -0
  27. package/src/eyeprolog-common-library.pl +44 -0
  28. package/src/eyeprolog-library.pl +113 -74
  29. package/src/index.js +2 -2
  30. package/src/playground-worker.js +2 -2
  31. package/src/solver.js +1 -1
  32. package/test/conformance/cases/builtins/extra_uuid_v4.pl +2 -2
  33. package/test/conformance/cases/lists/031_lists_aggregation_ordering.pl +1 -2
  34. package/test/conformance/cases/lists/045_sort_deduplicates_atoms.pl +2 -3
  35. package/test/conformance/cases/lists/087_sort_reverse_length.pl +2 -2
  36. package/test/conformance/cases/lists/extra_sort_iri_atoms.pl +1 -1
  37. package/test/conformance/cases/lists/sort_structured_terms.pl +1 -1
  38. package/test/run-examples.mjs +11 -19
  39. package/test/run-playground.mjs +5 -1
  40. package/test/run-regression.mjs +35 -19
  41. package/the-art-of-eyeprolog.md +37 -32
  42. package/src/library-source.js +0 -18
  43. package/src/library.js +0 -171
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
- **48 of the 50 are ordinary Prolog clauses** in `src/eyeprolog-library.pl` and
36
- are autoloaded in Node and the browser. Only `uuid/1` and `local_time/1` require
37
- host support. Portable text 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
@@ -1,8 +1,7 @@
1
1
  % Age checker adapted from Eyeling.
2
- % The example combines date literals, ISO-8601 duration values, local_time/1,
3
- % difference/3, and duration comparison. It deliberately uses the current
4
- % local date so the derived ageAbove/2 fact remains an executable temporal
5
- % check rather than a precomputed constant.
2
+ % The example combines date literals, ISO-8601 duration values, an explicit
3
+ % local_time/1 fact, difference/3, and duration comparison. Declaring the date
4
+ % as scenario data keeps the result reproducible across hosts and runs.
6
5
 
7
6
  %% goal: birthDay(X0, X1)
8
7
 
@@ -15,9 +14,10 @@
15
14
 
16
15
  birthDay(patH, '1944-08-21').
17
16
  duration(check, 'P80Y').
17
+ local_time('2026-05-30').
18
18
 
19
- % A person is above a duration if the local date minus the birthday is greater
20
- % than that duration.
19
+ % A person is above a duration if the declared local date minus the birthday
20
+ % is greater than that duration.
21
21
  ageAbove(S, A) :-
22
22
  birthDay(S, B),
23
23
  duration(check, A),
@@ -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) :-
@@ -3,7 +3,7 @@ why(
3
3
  ageAbove(patH, 'P80Y'),
4
4
  proof(
5
5
  goal(ageAbove(patH, 'P80Y')),
6
- by(rule("age.pl", clause(3))),
6
+ by(rule("age.pl", clause(4))),
7
7
  bindings([binding("S", patH), binding("A", 'P80Y'), binding("B", '1944-08-21'), binding("D", '2026-05-30'), binding("F", 'P81Y9M9D')]),
8
8
  uses([
9
9
  proof(
@@ -16,7 +16,7 @@ why(
16
16
  ),
17
17
  proof(
18
18
  goal(local_time('2026-05-30')),
19
- by(builtin(local_time, 1))
19
+ by(fact("age.pl", clause(3)))
20
20
  ),
21
21
  proof(
22
22
  goal(difference('2026-05-30', '1944-08-21', 'P81Y9M9D')),
@@ -35,12 +35,12 @@ why(
35
35
  holds_result(test, true),
36
36
  proof(
37
37
  goal(holds_result(test, true)),
38
- by(rule("age.pl", clause(4))),
39
- bindings([binding("S", patH)]),
38
+ by(rule("age.pl", clause(5))),
39
+ bindings([binding("__anon0", patH)]),
40
40
  uses([
41
41
  proof(
42
42
  goal(ageAbove(patH, 'P80Y')),
43
- by(rule("age.pl", clause(3))),
43
+ by(rule("age.pl", clause(4))),
44
44
  bindings([binding("S", patH), binding("A", 'P80Y'), binding("B", '1944-08-21'), binding("D", '2026-05-30'), binding("F", 'P81Y9M9D')]),
45
45
  uses([
46
46
  proof(
@@ -53,7 +53,7 @@ why(
53
53
  ),
54
54
  proof(
55
55
  goal(local_time('2026-05-30')),
56
- by(builtin(local_time, 1))
56
+ by(fact("age.pl", clause(3)))
57
57
  ),
58
58
  proof(
59
59
  goal(difference('2026-05-30', '1944-08-21', 'P81Y9M9D')),
@@ -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(275)))
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/examples/uuid.pl CHANGED
@@ -1,12 +1,12 @@
1
- % uuid/1 creates exactly one fresh version 4 UUID atom.
1
+ % uuid/3 creates a version 4 UUID atom from explicit random state.
2
2
  %
3
- % The UUID itself varies between runs, so this example checks its type and
4
- % canonical shape while returning a stable, reviewable answer.
3
+ % The same initial seed reproduces the same UUID across runs. Threading the
4
+ % returned seed into another call produces the next UUID in the sequence.
5
5
 
6
6
  %% goal: uuid_example(Result)
7
7
 
8
8
  uuid_example(true) :-
9
- uuid(UUID),
9
+ uuid(20260807, UUID, _),
10
10
  atom(UUID),
11
11
  atom_length(UUID, 36),
12
12
  sub_atom(UUID, 8, 1, 27, '-'),
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.11",
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",
@@ -681,7 +682,6 @@
681
682
  "le",
682
683
  "length",
683
684
  "list_to_set",
684
- "local_time",
685
685
  "log",
686
686
  "lowercase",
687
687
  "lt",
@@ -699,6 +699,7 @@
699
699
  "number_string",
700
700
  "once",
701
701
  "pow",
702
+ "random",
702
703
  "replace",
703
704
  "reverse",
704
705
  "rounded",
@@ -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('./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 './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 = {}) {
@@ -0,0 +1,133 @@
1
+ // Autoload the pure-Prolog EyeProlog library in Node and the browser.
2
+ import { createDefaultRegistry } from './iso.js';
3
+ import { parseClauses } from './parser.js';
4
+ import { fs, isNode } from './platform.js';
5
+
6
+ export const eyePrologNativeLibraryIndicators = Object.freeze([]);
7
+
8
+ export const eyePrologPortableLibraryIndicators = Object.freeze([
9
+ 'uuid/3',
10
+ 'difference/3',
11
+ 'apply/3',
12
+ 'maplist/3',
13
+ 'tan/2',
14
+ 'asin/2',
15
+ 'acos/2',
16
+ 'atan2/3',
17
+ 'lt/2',
18
+ 'gt/2',
19
+ 'le/2',
20
+ 'ge/2',
21
+ 'between/3',
22
+ 'smallest_divisor_from/3',
23
+ 'random/3',
24
+ 'matches/3',
25
+ 'split/3',
26
+ 'replace/4',
27
+ 'lowercase/2',
28
+ 'uppercase/2',
29
+ 'trim/2',
30
+ 'number_string/2',
31
+ 'atom_string/2',
32
+ 'term_string/2',
33
+ 'append/3',
34
+ 'string_concat/3',
35
+ 'contains/2',
36
+ 'matches/2',
37
+ 'join/3',
38
+ 'substring/4',
39
+ 'member/2',
40
+ 'select/3',
41
+ 'last/2',
42
+ 'nth0/3',
43
+ 'nth1/3',
44
+ 'set_nth0/4',
45
+ 'take/3',
46
+ 'drop/3',
47
+ 'slice/4',
48
+ 'reverse/2',
49
+ 'length/2',
50
+ 'sum_list/2',
51
+ 'min_list/2',
52
+ 'max_list/2',
53
+ 'list_to_set/2',
54
+ 'sort_unique/2',
55
+ 'countall/2',
56
+ 'sumall/3',
57
+ 'aggregate_min/5',
58
+ 'aggregate_max/5',
59
+ ]);
60
+
61
+ export const eyePrologLibraryIndicators = Object.freeze([
62
+ ...eyePrologNativeLibraryIndicators,
63
+ ...eyePrologPortableLibraryIndicators,
64
+ ]);
65
+
66
+ const autoloadedPrograms = new WeakSet();
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}`,
80
+ sourceMetadata: true,
81
+ }));
82
+
83
+ async function loadLibrarySource(url) {
84
+ if (isNode) return fs.readFileSync(url, 'utf8');
85
+ const response = await fetch(url);
86
+ if (!response.ok) throw new Error(`could not load EyeProlog library: ${response.status}`);
87
+ return response.text();
88
+ }
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
+
96
+ export function ensureEyePrologLibrary(program) {
97
+ if (autoloadedPrograms.has(program)) return program;
98
+
99
+ // User clauses already present in the Program stay first in clause order;
100
+ // the autoloaded library clauses are appended as defaults. This preserves
101
+ // useful source specializations such as length(numbers,N) while still making
102
+ // the relational length(List,N) library clauses available inside them.
103
+ let added = 0;
104
+ for (const template of portableClauseTemplates) {
105
+ // Clause terms are immutable; clone only the mutable indexing shell so the
106
+ // cached parse can be safely shared across independent Program instances.
107
+ const clause = {
108
+ ...template,
109
+ body: template.body.slice(),
110
+ index: program.clauses.length,
111
+ eyePrologLibraryPortable: true,
112
+ };
113
+ program.clauses.push(clause);
114
+ program.indexClause(clause);
115
+ added++;
116
+ }
117
+ if (added > 0) program.markRecursivePredicates();
118
+ autoloadedPrograms.add(program);
119
+ return program;
120
+ }
121
+
122
+ export function createEyePrologRegistry() {
123
+ const registry = createDefaultRegistry();
124
+ registry.eyePrologLibrary = true;
125
+ return registry;
126
+ }
127
+
128
+ let eyePrologRegistry = null;
129
+
130
+ export function getEyePrologRegistry() {
131
+ if (eyePrologRegistry == null) eyePrologRegistry = createEyePrologRegistry();
132
+ return eyePrologRegistry;
133
+ }