eyeprolog 0.5.11 → 0.5.12

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
@@ -32,9 +32,9 @@ 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** 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
38
38
  RDF tools emit lexical values as ISO atoms as well.
39
39
 
40
40
  ## 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),
@@ -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))),
38
+ by(rule("age.pl", clause(5))),
39
39
  bindings([binding("S", 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')),
@@ -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(299)))
16
16
  )
17
17
  ])
18
18
  )
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.12",
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
@@ -681,7 +681,6 @@
681
681
  "le",
682
682
  "length",
683
683
  "list_to_set",
684
- "local_time",
685
684
  "log",
686
685
  "lowercase",
687
686
  "lt",
@@ -699,6 +698,7 @@
699
698
  "number_string",
700
699
  "once",
701
700
  "pow",
701
+ "random",
702
702
  "replace",
703
703
  "reverse",
704
704
  "rounded",
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-library.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-library.js';
8
8
  import { Solver, nextFreshId } from './solver.js';
9
9
 
10
10
  export function whyProof(program, goal, options = {}) {
@@ -0,0 +1,118 @@
1
+ // Load and 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
+ 'call/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/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 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',
71
+ sourceMetadata: true,
72
+ });
73
+
74
+ async function loadLibrarySource(url) {
75
+ if (isNode) return fs.readFileSync(url, 'utf8');
76
+ const response = await fetch(url);
77
+ if (!response.ok) throw new Error(`could not load EyeProlog library: ${response.status}`);
78
+ return response.text();
79
+ }
80
+
81
+ export function ensureEyePrologLibrary(program) {
82
+ if (autoloadedPrograms.has(program)) return program;
83
+
84
+ // User clauses already present in the Program stay first in clause order;
85
+ // the autoloaded library clauses are appended as defaults. This preserves
86
+ // useful source specializations such as length(numbers,N) while still making
87
+ // the relational length(List,N) library clauses available inside them.
88
+ let added = 0;
89
+ for (const template of portableClauseTemplates) {
90
+ // Clause terms are immutable; clone only the mutable indexing shell so the
91
+ // cached parse can be safely shared across independent Program instances.
92
+ const clause = {
93
+ ...template,
94
+ body: template.body.slice(),
95
+ index: program.clauses.length,
96
+ eyePrologLibraryPortable: true,
97
+ };
98
+ program.clauses.push(clause);
99
+ program.indexClause(clause);
100
+ added++;
101
+ }
102
+ if (added > 0) program.markRecursivePredicates();
103
+ autoloadedPrograms.add(program);
104
+ return program;
105
+ }
106
+
107
+ export function createEyePrologRegistry() {
108
+ const registry = createDefaultRegistry();
109
+ registry.eyePrologLibrary = true;
110
+ return registry;
111
+ }
112
+
113
+ let eyePrologRegistry = null;
114
+
115
+ export function getEyePrologRegistry() {
116
+ if (eyePrologRegistry == null) eyePrologRegistry = createEyePrologRegistry();
117
+ return eyePrologRegistry;
118
+ }
@@ -3,8 +3,8 @@
3
3
  % This file is autoloaded for the EyeProlog runtime and is intentionally
4
4
  % written against the project's ISO compatibility profile. Text-facing
5
5
  % predicates accept ISO atoms or proper lists of one-character atoms; newly
6
- % produced text defaults to atoms. Only uuid/1 and local_time/1 remain native
7
- % host built-ins because ISO Prolog has no entropy or wall-clock primitive.
6
+ % produced text defaults to atoms. Every public predicate in this file uses
7
+ % only the project's ISO Prolog profile; none requires host support.
8
8
 
9
9
  % ---------- text representation helpers ----------
10
10
 
@@ -140,6 +140,72 @@ eyeprolog__between(Low, High, Value) :-
140
140
  !,
141
141
  eyeprolog__between(Next, High, Value).
142
142
 
143
+ % A Park-Miller generator with explicit state. Threading Seed into the next
144
+ % call makes a sequence reproducible without mutable runtime state. Schrage's
145
+ % method keeps every intermediate integer within the exact 32-bit range.
146
+ random(Seed0, Value, Seed) :-
147
+ integer(Seed0),
148
+ eyeprolog__random_normalize_seed(Seed0, Normalized),
149
+ High is Normalized // 44488,
150
+ Low is Normalized mod 44488,
151
+ Candidate is 48271 * Low - 3399 * High,
152
+ eyeprolog__random_wrap(Candidate, Seed),
153
+ Value is (Seed - 1) / 2147483646.
154
+
155
+ eyeprolog__random_normalize_seed(Seed, 1) :-
156
+ 0 is Seed mod 2147483647,
157
+ !.
158
+ eyeprolog__random_normalize_seed(Seed, Normalized) :-
159
+ Normalized is Seed mod 2147483647.
160
+
161
+ eyeprolog__random_wrap(Candidate, Candidate) :- Candidate > 0, !.
162
+ eyeprolog__random_wrap(Candidate, Seed) :- Seed is Candidate + 2147483647.
163
+
164
+ % Seed-threaded UUID version 4 generation. The version and variant nibbles are
165
+ % fixed by RFC 9562; all other nibbles come from random/3. Reusing Seed0
166
+ % reproduces the same UUID, while threading Seed produces a deterministic
167
+ % sequence of different UUIDs.
168
+ uuid(Seed0, UUID, Seed) :-
169
+ eyeprolog__uuid_hex(8, Seed0, Group1, Seed1),
170
+ eyeprolog__uuid_hex(4, Seed1, Group2, Seed2),
171
+ eyeprolog__uuid_hex(3, Seed2, Group3, Seed3),
172
+ random(Seed3, _, Seed4),
173
+ VariantValue is 8 + Seed4 mod 4,
174
+ eyeprolog__hex_digit(VariantValue, Variant),
175
+ eyeprolog__uuid_hex(3, Seed4, Group4, Seed5),
176
+ 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),
181
+ atom_chars(UUID, Chars).
182
+
183
+ eyeprolog__uuid_hex(0, Seed, [], Seed).
184
+ eyeprolog__uuid_hex(Count, Seed0, [Digit|Digits], Seed) :-
185
+ Count > 0,
186
+ random(Seed0, _, Seed1),
187
+ Value is Seed1 mod 16,
188
+ eyeprolog__hex_digit(Value, Digit),
189
+ NextCount is Count - 1,
190
+ eyeprolog__uuid_hex(NextCount, Seed1, Digits, Seed).
191
+
192
+ eyeprolog__hex_digit(0, '0').
193
+ eyeprolog__hex_digit(1, '1').
194
+ eyeprolog__hex_digit(2, '2').
195
+ eyeprolog__hex_digit(3, '3').
196
+ eyeprolog__hex_digit(4, '4').
197
+ eyeprolog__hex_digit(5, '5').
198
+ eyeprolog__hex_digit(6, '6').
199
+ eyeprolog__hex_digit(7, '7').
200
+ eyeprolog__hex_digit(8, '8').
201
+ eyeprolog__hex_digit(9, '9').
202
+ eyeprolog__hex_digit(10, a).
203
+ eyeprolog__hex_digit(11, b).
204
+ eyeprolog__hex_digit(12, c).
205
+ eyeprolog__hex_digit(13, d).
206
+ eyeprolog__hex_digit(14, e).
207
+ eyeprolog__hex_digit(15, f).
208
+
143
209
  smallest_divisor_from(N, Start, Divisor) :-
144
210
  N >= 0,
145
211
  Start > 0,
package/src/index.js CHANGED
@@ -18,7 +18,7 @@ export {
18
18
  eyePrologLibraryIndicators,
19
19
  eyePrologNativeLibraryIndicators,
20
20
  eyePrologPortableLibraryIndicators,
21
- } from './library.js';
21
+ } from './eyeprolog-library.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 './library.js';
29
+ import { getEyePrologRegistry } from './eyeprolog-library.js';
30
30
  import { parseGoalText } from './parser.js';
31
31
 
32
32
  export function run(source, options = {}) {
@@ -2,7 +2,7 @@
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
4
  import { run } from './index.js?playground=20260803c';
5
- import { createEyePrologRegistry } from './library.js?playground=20260803c';
5
+ import { createEyePrologRegistry } from './eyeprolog-library.js?playground=20260803c';
6
6
 
7
7
  const registry = createEyePrologRegistry();
8
8
 
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 './library.js';
8
+ import { ensureEyePrologLibrary, getEyePrologRegistry } from './eyeprolog-library.js';
9
9
  import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
10
10
  import { StreamManager } from './io.js';
11
11
 
@@ -1,6 +1,6 @@
1
- % uuid/1 is deterministic and creates a canonical version 4 UUID atom.
1
+ % uuid/3 deterministically creates a canonical version 4 UUID atom from a seed.
2
2
  answer(uuid_v4, true) :-
3
- uuid(UUID),
3
+ uuid(1, UUID, _),
4
4
  atom(UUID),
5
5
  atom_length(UUID, 36),
6
6
  sub_atom(UUID, 8, 1, 27, '-'),
@@ -14,7 +14,6 @@ const packageRoot = path.resolve(root, '..');
14
14
  const examplesDir = path.join(packageRoot, 'examples');
15
15
  const expectedDir = path.join(examplesDir, 'output');
16
16
  const expectedProofDir = path.join(examplesDir, 'proof');
17
- const fixedExampleDate = '2026-05-30';
18
17
 
19
18
  export const proofExamples = [
20
19
  'access-control-policy.pl',
@@ -124,25 +123,18 @@ function runProofExample(name) {
124
123
  }
125
124
 
126
125
  function runProgramExample(programFile, filename, options) {
127
- const oldLocalTime = process.env.EYEPROLOG_LOCAL_TIME;
128
- process.env.EYEPROLOG_LOCAL_TIME = fixedExampleDate;
126
+ const text = fs.readFileSync(programFile, 'utf8');
127
+ const expectedExit = text.match(/^%\s*expect-exit:\s*(\d+)\s*$/m);
128
+ const program = Program.parseSources([{ text, filename }], {
129
+ sourceMetadata: options.proof,
130
+ });
129
131
  try {
130
- const text = fs.readFileSync(programFile, 'utf8');
131
- const expectedExit = text.match(/^%\s*expect-exit:\s*(\d+)\s*$/m);
132
- const program = Program.parseSources([{ text, filename }], {
133
- sourceMetadata: options.proof,
134
- });
135
- try {
136
- const result = run(program, { ...options, goals: goalsInProgramOrder(program, text) });
137
- if (expectedExit) throw new Error(`${filename} expected exit ${expectedExit[1]}, but reasoning succeeded`);
138
- return result.stdout;
139
- } catch (error) {
140
- if (expectedExit && error?.code === Number(expectedExit[1])) return error.stdout ?? '';
141
- throw error;
142
- }
143
- } finally {
144
- if (oldLocalTime == null) delete process.env.EYEPROLOG_LOCAL_TIME;
145
- else process.env.EYEPROLOG_LOCAL_TIME = oldLocalTime;
132
+ const result = run(program, { ...options, goals: goalsInProgramOrder(program, text) });
133
+ if (expectedExit) throw new Error(`${filename} expected exit ${expectedExit[1]}, but reasoning succeeded`);
134
+ return result.stdout;
135
+ } catch (error) {
136
+ if (expectedExit && error?.code === Number(expectedExit[1])) return error.stdout ?? '';
137
+ throw error;
146
138
  }
147
139
  }
148
140
 
@@ -94,7 +94,7 @@ export async function runPlayground(reporter = new TestReporter()) {
94
94
  await withStaticServer(async (baseUrl) => {
95
95
  const modules = await crawlModuleGraph(new URL('src/playground-worker.js?playground=test', baseUrl));
96
96
  assert(modules.size >= 10, `expected a substantial worker module graph, got ${modules.size}`);
97
- assert([...modules].some((url) => url.includes('/src/library.js')), 'EyeProlog library missing from worker graph');
97
+ assert([...modules].some((url) => url.includes('/src/eyeprolog-library.js')), 'EyeProlog library missing from worker graph');
98
98
  assert([...modules].some((url) => url.includes('/src/solver.js')), 'solver missing from worker graph');
99
99
  });
100
100
  });
@@ -177,15 +177,23 @@ why(
177
177
  },
178
178
  },
179
179
  {
180
- name: 'EYEPROLOG_LOCAL_TIME fixes local_time builtin',
180
+ name: 'seeded random/3 sequence is reproducible',
181
181
  run: () => {
182
- const result = runCli(['--goal', 'local_time_answer(D)', '-'], {
183
- input: 'local_time_answer(D) :- local_time(D).\n',
184
- env: { EYEPROLOG_LOCAL_TIME: '2024-01-02' },
185
- });
186
- assertEqual(result.status, 0, 'exit status');
187
- assertEqual(result.stdout, "local_time_answer('2024-01-02').\n", 'stdout');
188
- assertEqual(result.stderr, '', 'stderr');
182
+ const result = run(
183
+ 'seeded(A, B, C, Seeds) :- random(1, A, S1), random(S1, B, S2), random(1, C, S3), Seeds = [S1, S2, S3].\n',
184
+ { goal: 'seeded(A, B, C, Seeds)' },
185
+ );
186
+ assertEqual(result.stdout, 'seeded(0.00002247747035927835, 0.085032448717423201, 0.00002247747035927835, [48271, 182605794, 48271]).\n', 'stdout');
187
+ },
188
+ },
189
+ {
190
+ name: 'seeded uuid/3 sequence is reproducible',
191
+ run: () => {
192
+ const result = run(
193
+ 'seeded_uuid(U1, U2, true) :- uuid(1, U1, S1), uuid(1, U1, _), uuid(S1, U2, _), U1 \\= U2.\n',
194
+ { goal: 'seeded_uuid(U1, U2, Same)' },
195
+ );
196
+ assertEqual(result.stdout, "seeded_uuid('f26d1319-3f3f-4bd9-b92f-f414794a43b5', '4be874d3-166b-4107-b0dc-9c53074b3de1', true).\n", 'stdout');
189
197
  },
190
198
  },
191
199
  {
@@ -576,7 +584,7 @@ function documentationSyncCases() {
576
584
  '[Book — *The Art of EyeProlog*](https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog)',
577
585
  'README links to the book',
578
586
  );
579
- for (const filename of ['src/iso.js', 'src/library.js', 'src/playground-worker.js']) {
587
+ for (const filename of ['src/iso.js', 'src/eyeprolog-library.js', 'src/playground-worker.js']) {
580
588
  assertEqual(fs.existsSync(path.join(packageRoot, filename)), true, `${filename} exists`);
581
589
  assertIncludes(book, filename, `book documents ${filename}`);
582
590
  }
@@ -1007,19 +1015,21 @@ open(X) :- candidate(X), \\+ closed(X).
1007
1015
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
1008
1016
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
1009
1017
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
1010
- assertEqual(library.defs.size, 117, 'ISO plus host registry size');
1011
- assertEqual(registeredNativeEyePrologLibraryNames().length, 2, 'public native EyeProlog builtin count');
1012
- assertEqual(eyePrologPortableLibraryIndicators.length, 48, 'portable Prolog library count');
1013
- assertEqual(eyePrologNativeLibraryIndicators.length, 2, 'native host library count');
1014
- assertEqual(eyePrologNativeLibraryIndicators.join(','), 'uuid/1,local_time/1', 'native host library stays minimal');
1018
+ assertEqual(library.defs.size, 115, 'EyeProlog registry contains only ISO host definitions');
1019
+ assertEqual(registeredNativeEyePrologLibraryNames().length, 0, 'public native EyeProlog builtin count');
1020
+ assertEqual(eyePrologPortableLibraryIndicators.length, 50, 'portable Prolog library count');
1021
+ assertEqual(eyePrologNativeLibraryIndicators.length, 0, 'native host library count');
1022
+ assertEqual(eyePrologNativeLibraryIndicators.join(','), '', 'no EyeProlog library predicate requires host support');
1015
1023
  assertEqual(eyePrologLibraryIndicators.length, 50, 'complete EyeProlog library surface');
1016
1024
  assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
1017
1025
  assertEqual(library.get('smallest_divisor_from', 3), null, 'smallest_divisor_from/3 remains portable Prolog');
1026
+ assertEqual(library.get('random', 3), null, 'random/3 remains portable Prolog');
1027
+ assertEqual(library.get('local_time', 1), null, 'local_time/1 is not a host builtin');
1018
1028
  assertEqual(library.get('eyeprolog__string_atom', 2), null, 'private string adapter is absent');
1019
1029
  assertEqual(library.get('append', 3), null, 'append/3 moved to portable Prolog');
1020
1030
  assertEqual(library.get('maplist', 3), null, 'maplist/3 moved to portable Prolog');
1021
1031
  assertEqual(library.get('matches', 3), null, 'matches/3 moved to portable Prolog');
1022
- assertEqual(library.get('uuid', 1)?.deterministic, true, 'uuid/1 deterministic metadata');
1032
+ assertEqual(library.get('uuid', 3), null, 'uuid/3 remains portable Prolog');
1023
1033
  for (const [name, arity] of [['not_member', 2], ['head', 2], ['rest', 2], ['min', 3], ['max', 3]]) {
1024
1034
  assertEqual(library.get(name, arity), null, `${name}/${arity} removed from library`);
1025
1035
  }
@@ -1037,9 +1047,11 @@ open(X) :- candidate(X), \\+ closed(X).
1037
1047
  assertEqual(betweenGenerator.cutRecursive, true, 'portable between generator has deterministic recursive control');
1038
1048
  assertEqual(betweenGenerator.tabled, false, 'portable between generator avoids suffix answer tables');
1039
1049
  assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'eyeprolog-library.pl')), true, 'portable Prolog source exists');
1040
- assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'library-source.js')), true, 'portable source loader exists');
1050
+ assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'eyeprolog-library.js')), true, 'portable source loader exists');
1051
+ assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'library-source.js')), false, 'duplicate source loader is absent');
1041
1052
  assertEqual(fs.existsSync(path.join(packageRoot, 'src', 'portable-library.js')), false, 'obsolete duplicate module remains absent');
1042
1053
  assertEqual(run(program, { goal: 'answer(X)' }).stdout, 'answer([a, b]).\n', 'autoloaded append execution');
1054
+ assertEqual(Boolean(program.findGroup('random', 3)), true, 'random/3 is autoloaded as a clause group');
1043
1055
  },
1044
1056
  },
1045
1057
  {
@@ -1053,8 +1065,9 @@ portable_check(A, B, C) :- lowercase('HELLO', A), replace('banana', 'na', 'NA',
1053
1065
  const goal = parseGoalText('portable_check(A, B, C)');
1054
1066
  const answers = [...solver.solve([goal], new Env(), 0)].map((env) => termToString(goal, env, true));
1055
1067
  assertEqual(answers.join('\n'), "portable_check(hello, baNANA, [x, y])", 'ISO-only portable execution');
1056
- assertEqual(program.findGroup('uuid', 1), null, 'uuid/1 remains outside portable file');
1057
- assertEqual(program.findGroup('local_time', 1), null, 'local_time/1 remains outside portable file');
1068
+ assertEqual(Boolean(program.findGroup('uuid', 3)), true, 'uuid/3 is implemented in the portable file');
1069
+ assertEqual(program.findGroup('uuid', 1), null, 'obsolete uuid/1 is absent');
1070
+ assertEqual(program.findGroup('local_time', 1), null, 'local_time/1 is absent from the library');
1058
1071
  },
1059
1072
  },
1060
1073
  {
@@ -1699,7 +1712,7 @@ function playgroundStaticIssues() {
1699
1712
  if (!html.includes("new URL('./src/playground-worker.js?playground=")) issues.push('playground must cache-bust its dedicated module worker');
1700
1713
  if (!html.includes("new Worker(workerUrl, { type: 'module' })")) issues.push('playground must launch the dedicated module worker');
1701
1714
  const workerText = fs.readFileSync(path.join(packageRoot, 'src', 'playground-worker.js'), 'utf8');
1702
- if (!workerText.includes("from './library.js?playground=") ||
1715
+ if (!workerText.includes("from './eyeprolog-library.js?playground=") ||
1703
1716
  !workerText.includes('createEyePrologRegistry') ||
1704
1717
  !workerText.includes('executePlaygroundRequest')) {
1705
1718
  issues.push('playground worker must install the EyeProlog library registry');
@@ -1710,10 +1710,10 @@ truncate search; it does not prove that no further answer exists.
1710
1710
 
1711
1711
  The source layout mirrors the language boundary. `src/iso.js` contains the
1712
1712
  isolated ISO processor predicates and registry. `src/eyeprolog-library.pl` is
1713
- the portable EyeProlog library: 48 public predicates written as ordinary
1714
- Prolog clauses against that ISO profile. `src/library-source.js` loads the same
1715
- file in Node and the browser, while `src/library.js` owns autoload integration
1716
- and the two unavoidable public host predicates, `uuid/1` and `local_time/1`.
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.
1717
1717
  The RDF tools emit IRI and literal lexical values as ISO atoms, matching the
1718
1718
  portable text API without a host representation adapter.
1719
1719
 
@@ -5253,17 +5253,15 @@ so side effects occur in Prolog execution order.
5253
5253
  ### The EyeProlog library
5254
5254
 
5255
5255
  EyeProlog exposes **50 library predicate indicators** in addition to the 115
5256
- indicators in its isolated ISO profile. Of those 50, **48 are ordinary Prolog
5257
- clauses** in `src/eyeprolog-library.pl`; only **`uuid/1` and `local_time/1` are
5258
- native host predicates** because ISO Prolog provides neither entropy nor a wall
5259
- clock. The resulting normal EyeProlog language surface is therefore **165
5260
- public predicate indicators**. Internally, the runtime registry has 117
5261
- definitions: 115 ISO predicates and the 2 public host predicates.
5256
+ 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
+ normal EyeProlog language surface is therefore **165 public predicate
5259
+ indicators**. Internally, the runtime registry contains only the 115 ISO
5260
+ definitions; the EyeProlog relations are autoloaded source clauses.
5262
5261
 
5263
5262
  The portable file is autoloaded once into every `Program` used with the
5264
- EyeProlog registry. `src/library-source.js` loads it from the package in Node or
5265
- through `fetch()` in the browser; `src/library.js` performs the autoload and
5266
- registers the two host predicates.
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.
5267
5265
  The isolated ISO-only registry remains
5268
5266
  available through `createDefaultRegistry()` and `getDefaultRegistry()` for
5269
5267
  conformance work and advanced embedders. Source clauses sharing a portable
@@ -5283,11 +5281,11 @@ standard fallback relation.
5283
5281
  | `join/3`, `substring/4` |
5284
5282
  | `countall/2`, `sumall/3` |
5285
5283
  | `aggregate_min/5`, `aggregate_max/5` |
5286
- | `between/3`, `smallest_divisor_from/3` |
5284
+ | `between/3`, `smallest_divisor_from/3`, `random/3` |
5287
5285
  | `maplist/3` |
5288
5286
  | `acos/2`, `asin/2`, `atan2/3`, `tan/2` |
5289
5287
  | `lt/2`, `le/2`, `gt/2`, `ge/2` |
5290
- | `uuid/1`, `local_time/1`, `difference/3` |
5288
+ | `uuid/3`, `difference/3` |
5291
5289
  | `matches/3` |
5292
5290
  | `call/3` |
5293
5291
  | `split/3`, `replace/4` |
@@ -5296,10 +5294,11 @@ standard fallback relation.
5296
5294
 
5297
5295
  <!-- eyeprolog-library-catalog:end -->
5298
5296
 
5299
- `uuid/1` creates one fresh version 4 UUID atom. It is deterministic in
5300
- the Prolog sense: each call has exactly one solution, although the generated atom
5301
- is intentionally different between calls. The implementation uses the Web Crypto API where available and remains
5302
- portable across Node and the browser playground.
5297
+ `uuid(+Seed0,-UUID,-Seed)` creates a version 4 UUID atom using `random/3`.
5298
+ Passing the returned seed to the next call produces the next UUID; restarting
5299
+ with the same integer seed reproduces the same sequence exactly. This explicit
5300
+ state replaces hidden host entropy and behaves identically in Node and the
5301
+ browser playground.
5303
5302
 
5304
5303
  On the command line, the EyeProlog library is already present:
5305
5304
 
@@ -5335,13 +5334,14 @@ the corresponding predicate.
5335
5334
  | --- | --- |
5336
5335
  | `tan/2`, `asin/2`, `acos/2`, `atan2/3` | Floating-point functions not supplied as evaluable functions by ISO/IEC 13211-1. |
5337
5336
  | `lt(+A,+B)`, `le(+A,+B)`, `gt(+A,+B)`, `ge(+A,+B)` | Compare integers exactly, finite numeric text numerically, `PnYnMnD` duration text component-wise, and other lexical values by string order. These differ from ISO arithmetic comparison and standard term order. |
5338
- | `local_time(-Date)` | The one host relation in this group. Produces the host-local calendar date as atom `'YYYY-MM-DD'`. Tests and reproducible hosts may set `EYEPROLOG_LOCAL_TIME`. |
5337
+ | `random(+Seed0,-Value,-Seed)` | Portable Park-Miller generator with explicit state. `Value` is in `[0,1)`; pass the returned integer `Seed` to the next call. The same initial integer seed always reproduces the same sequence. |
5339
5338
  | `difference(+End,+Start,-Duration)` | Portable Prolog. Computes a nonnegative calendar difference between ISO date atoms/character lists and returns atom `'PnYnMnD'`. Invalid dates or an end before the start fail. |
5340
5339
 
5341
5340
  ```eyeprolog
5342
5341
  answer(square, S) :- (S is 12 * 12).
5343
5342
  answer(day_count, N) :- between(3, 5, N).
5344
5343
  answer(age, D) :- difference('2026-07-28', '2020-05-20', D).
5344
+ answer(random_pair, [A,B]) :- random(42, A, S), random(S, B, _).
5345
5345
  eyeprolog --goal 'answer(Kind, Value)' program.pl
5346
5346
  ```
5347
5347
 
@@ -5778,7 +5778,7 @@ studies.
5778
5778
  | [Socket Age](https://github.com/eyereasoner/eyeprolog/blob/main/examples/socket-age.pl) | Socket and plug declarations for an age-reasoning module with registry, policy, and clock providers. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/socket-age.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/socket-age.pl) |
5779
5779
  | [Socket Family](https://github.com/eyereasoner/eyeprolog/blob/main/examples/socket-family.pl) | Socket and plug declarations for a family-reasoning module that expects a `parent/2` provider. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/socket-family.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/socket-family.pl) |
5780
5780
  | [Socrates](https://github.com/eyereasoner/eyeprolog/blob/main/examples/socrates.pl) | A fact and one rule turn the classical syllogism into a ground derivation. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/socrates.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/socrates.pl) |
5781
- | [UUID](https://github.com/eyereasoner/eyeprolog/blob/main/examples/uuid.pl) | `uuid/1` creates one version 4 UUID atom; the example validates its shape while keeping the checked answer stable. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/uuid.pl) |
5781
+ | [UUID](https://github.com/eyereasoner/eyeprolog/blob/main/examples/uuid.pl) | `uuid/3` reproducibly creates one version 4 UUID atom from explicit random state; the example validates its canonical shape. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/uuid.pl) |
5782
5782
  | [Witch](https://github.com/eyereasoner/eyeprolog/blob/main/examples/witch.pl) | Burn the witch, adapted from Eyeling's examples/witch.n3. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/witch.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/witch.pl) |
5783
5783
 
5784
5784
  Suggested path: Socrates → Age → Ancestor → Derived rule → Reusable built-ins.
@@ -1,18 +0,0 @@
1
- // Load the portable EyeProlog Prolog-library source from the package assets.
2
- // eyeprolog-library.pl is the source of truth for all 48 portable public
3
- // library predicates.
4
- import { fs, isNode } from './platform.js';
5
-
6
- const portableLibraryUrl = new URL('./eyeprolog-library.pl', import.meta.url);
7
-
8
- async function loadSource(url, label) {
9
- if (isNode) return fs.readFileSync(url, 'utf8');
10
- const response = await fetch(url);
11
- if (!response.ok) throw new Error(`could not load ${label}: ${response.status}`);
12
- return response.text();
13
- }
14
-
15
- export const eyePrologPortableLibrarySource = await loadSource(
16
- portableLibraryUrl,
17
- 'EyeProlog portable library',
18
- );
package/src/library.js DELETED
@@ -1,171 +0,0 @@
1
- // EyeProlog library integration.
2
- //
3
- // 48 of the 50 public library predicates are implemented in plain Prolog in
4
- // eyeprolog-library.pl and autoloaded into every Solver that uses the EyeProlog
5
- // registry. Only uuid/1 and local_time/1 remain native because ISO Prolog has
6
- // no standard entropy or wall-clock primitive.
7
- import {
8
- atom,
9
- unify,
10
- } from './term.js';
11
- import {
12
- BuiltinRegistry,
13
- isoBuiltins,
14
- } from './iso.js';
15
- import { parseClauses } from './parser.js';
16
- import { eyePrologPortableLibrarySource } from './library-source.js';
17
-
18
- export const eyePrologNativeLibraryIndicators = Object.freeze([
19
- 'uuid/1',
20
- 'local_time/1',
21
- ]);
22
-
23
- export const eyePrologPortableLibraryIndicators = Object.freeze([
24
- 'difference/3',
25
- 'call/3',
26
- 'maplist/3',
27
- 'tan/2',
28
- 'asin/2',
29
- 'acos/2',
30
- 'atan2/3',
31
- 'lt/2',
32
- 'gt/2',
33
- 'le/2',
34
- 'ge/2',
35
- 'between/3',
36
- 'smallest_divisor_from/3',
37
- 'matches/3',
38
- 'split/3',
39
- 'replace/4',
40
- 'lowercase/2',
41
- 'uppercase/2',
42
- 'trim/2',
43
- 'number_string/2',
44
- 'atom_string/2',
45
- 'term_string/2',
46
- 'append/3',
47
- 'string_concat/3',
48
- 'contains/2',
49
- 'matches/2',
50
- 'join/3',
51
- 'substring/4',
52
- 'member/2',
53
- 'select/3',
54
- 'last/2',
55
- 'nth0/3',
56
- 'nth1/3',
57
- 'set_nth0/4',
58
- 'take/3',
59
- 'drop/3',
60
- 'slice/4',
61
- 'reverse/2',
62
- 'length/2',
63
- 'sum_list/2',
64
- 'min_list/2',
65
- 'max_list/2',
66
- 'list_to_set/2',
67
- 'sort/2',
68
- 'countall/2',
69
- 'sumall/3',
70
- 'aggregate_min/5',
71
- 'aggregate_max/5',
72
- ]);
73
-
74
- export const eyePrologLibraryIndicators = Object.freeze([
75
- ...eyePrologNativeLibraryIndicators,
76
- ...eyePrologPortableLibraryIndicators,
77
- ]);
78
-
79
- const portableIndicatorSet = new Set(eyePrologPortableLibraryIndicators);
80
- const autoloadedPrograms = new WeakSet();
81
- const portableClauseTemplates = parseClauses(eyePrologPortableLibrarySource, {
82
- filename: 'src/eyeprolog-library.pl',
83
- sourceMetadata: true,
84
- });
85
-
86
-
87
- export function ensureEyePrologLibrary(program) {
88
- if (autoloadedPrograms.has(program)) return program;
89
-
90
- // User clauses already present in the Program stay first in clause order;
91
- // the autoloaded library clauses are appended as defaults. This preserves
92
- // useful source specializations such as length(numbers,N) while still making
93
- // the relational length(List,N) library clauses available inside them.
94
- let added = 0;
95
- for (const template of portableClauseTemplates) {
96
- // Clause terms are immutable; clone only the mutable indexing shell so the
97
- // cached parse can be safely shared across independent Program instances.
98
- const clause = {
99
- ...template,
100
- body: template.body.slice(),
101
- index: program.clauses.length,
102
- eyePrologLibraryPortable: true,
103
- };
104
- program.clauses.push(clause);
105
- program.indexClause(clause);
106
- added++;
107
- }
108
- if (added > 0) program.markRecursivePredicates();
109
- autoloadedPrograms.add(program);
110
- return program;
111
- }
112
-
113
- export function createEyePrologRegistry() {
114
- const registry = new BuiltinRegistry();
115
- registry.eyePrologLibrary = true;
116
-
117
- registry.add('uuid', 1, uuidBuiltin, {
118
- deterministic: true,
119
- eyePrologLibrary: true,
120
- });
121
- registry.add('local_time', 1, localTimeBuiltin, {
122
- deterministic: true,
123
- eyePrologLibrary: true,
124
- });
125
- // ISO definitions take precedence where names overlap and remain identifiable
126
- // as ISO rather than EyeProlog-library predicates.
127
- isoBuiltins.register(registry);
128
- return registry;
129
- }
130
-
131
- let eyePrologRegistry = null;
132
-
133
- export function getEyePrologRegistry() {
134
- if (eyePrologRegistry == null) eyePrologRegistry = createEyePrologRegistry();
135
- return eyePrologRegistry;
136
- }
137
-
138
- function* uuidBuiltin({ goal, env }) {
139
- const next = env.clone();
140
- if (unify(goal.args[0], atom(randomUuidV4()), next)) yield next;
141
- }
142
-
143
- function* localTimeBuiltin({ goal, env }) {
144
- const next = env.clone();
145
- if (unify(goal.args[0], atom(localDateText()), next)) yield next;
146
- }
147
-
148
- function randomUuidV4() {
149
- const cryptoApi = globalThis.crypto;
150
- if (typeof cryptoApi?.randomUUID === 'function') return cryptoApi.randomUUID();
151
- const bytes = new Uint8Array(16);
152
- if (typeof cryptoApi?.getRandomValues === 'function') {
153
- cryptoApi.getRandomValues(bytes);
154
- } else {
155
- for (let index = 0; index < bytes.length; index++) bytes[index] = Math.floor(Math.random() * 256);
156
- }
157
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
158
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
159
- const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0'));
160
- return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`;
161
- }
162
-
163
- function localDateText() {
164
- const fixed = typeof process !== 'undefined' ? process.env?.EYEPROLOG_LOCAL_TIME : null;
165
- if (fixed) return fixed;
166
- const now = new Date();
167
- const yyyy = now.getFullYear();
168
- const mm = String(now.getMonth() + 1).padStart(2, '0');
169
- const dd = String(now.getDate()).padStart(2, '0');
170
- return `${yyyy}-${mm}-${dd}`;
171
- }