eyeprolog 1.5.5 → 1.5.7

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
@@ -56,14 +56,10 @@ printf 'human(socrates).\nmortal(X) :- human(X).\n' |
56
56
  npx --yes eyeprolog --proof --goal 'mortal(socrates)' -
57
57
  ```
58
58
 
59
- ## Native forward rules (`:+`)
60
- Normal mode executes top-level `Conclusion :+ Premise` rules natively when no explicit
61
- `-g/--goal` is supplied. `true :+ Goal` prints answers and `false :+ Goal` emits a
62
- `fuse/1`; JavaScript `run()` follows the same rule. The bundled `library(eyelet)`
63
- exports the `:+` operator, `stable/1`, and `becomes/2`. Strict ISO mode disables this
64
- extension. Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL;
65
- use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce
66
- operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
59
+ ## Eyelet forward rules (`:+`)
60
+ Normal mode executes top-level `Conclusion :+ Premise` rules through the bundled Prolog `library(eyelet)` driver when no explicit `-g/--goal` is supplied. `true :+ Goal` prints answers and `false :+ Goal` emits a `fuse/1`; JavaScript `run()` follows the same rule. The module exports `:+`, `stable/1`, and `becomes/2`, and implements rule selection, fixed-point rounds, skolemization, state replacement, and duplicate suppression in Prolog. JavaScript retains only syntax recognition, static dependency/autoload planning, driver bootstrap, and private mutability/output adapters; it does not implement the fixed-point semantics. Strict ISO mode disables this extension.
61
+
62
+ Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL; use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
67
63
 
68
64
  ## Links
69
65
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.5",
6
+ "version": "1.5.7",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/execute.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // Shared goal preparation and execution for the CLI and embedding API.
2
- import { ATOM, COMPOUND, NUMBER, VAR, Env, atom, compound, copyResolved, flattenConjunction, numberTerm, termIsGround, variable } from './term.js';
2
+ import { ATOM, COMPOUND, NUMBER, VAR, Env, compound, copyResolved, termIsGround, variable } from './term.js';
3
3
  import { parseGoalText } from './parser.js';
4
4
  import { HaltSignal, PrologError } from './iso.js';
5
5
  import { formatTermForWrite } from './write.js';
6
- import { modulePredicateKey } from './program-indexing.js';
6
+ import { loadBundledProgramLibrary } from './program.js';
7
7
 
8
8
  export function normalizeGoals(requestedGoals, solver) {
9
9
  return requestedGoals.map((requestedGoal) => {
@@ -57,14 +57,11 @@ function currentWriteOptions(program, solver) {
57
57
  }
58
58
 
59
59
 
60
- // Native EyeProlog forward-rule extension.
60
+ // Eyelet forward-rule bootstrap.
61
61
  //
62
- // Source terms `Conclusion :+ Premise` remain ordinary :+/2 clauses so they
63
- // can be inspected and even derived by Prolog code. The closure driver below
64
- // replaces Eyelet's Prolog-level meta-interpreter: it asks the solver directly
65
- // for (: +)/2 rule instances whose premises hold, materializes novel
66
- // conclusions, and repeats to a fixed point. Query and fuse rules retain the
67
- // Eyelet conventions `true :+ Goal` and `false :+ Goal`.
62
+ // The :+/2 fixed-point semantics live in library(eyelet). This JavaScript
63
+ // adapter only ensures that library is loaded, invokes its Prolog driver, and
64
+ // converts the driver's recorded answer/fuse events to the public callbacks.
68
65
  export function hasForwardRules(program) {
69
66
  return program.findGroup(':+', 2) != null;
70
67
  }
@@ -74,293 +71,65 @@ export function executeForwardRules(program, solver, {
74
71
  onFuse = () => {},
75
72
  onDiagnostic = () => {},
76
73
  } = {}) {
77
- const group = program.findGroup(':+', 2);
78
- if (group == null) return { haltCode: null, rounds: 0, derived: 0 };
74
+ if (!hasForwardRules(program)) return { haltCode: null, rounds: 0, derived: 0 };
79
75
 
76
+ // Load the Prolog driver without importing a bootstrap-only predicate into
77
+ // user or widening library(eyelet)'s public API.
78
+ loadBundledProgramLibrary(program, 'eyelet');
80
79
  solver.runInitializations();
81
- makeGroupDynamic(program, group);
82
- predeclareForwardTerms(program, solver, group);
83
- setSingletonFact(program, 'closure', [numberTerm(0)]);
84
- setSingletonFact(program, 'limit', [numberTerm(-1)]);
85
80
 
86
- const seenAnswers = new Set();
87
- let rounds = 0;
88
- let derived = 0;
89
- let haltCode = null;
90
-
91
- // Query-only Eyelet files are common and should be as cheap as ordinary
92
- // EyeProlog -g goals. Avoid routing their premises through a variable
93
- // call/1 and :+/2 clause-selection meta path. This is especially important
94
- // for deep deterministic workloads such as Ackermann and the evolutionary
95
- // example.
96
- if (forwardGroupIsPlainQueries(group)) {
97
- for (const clause of group.clauses) {
98
- const conclusion = clause.head.args[0];
99
- const premise = clause.head.args[1];
100
- const child = solver.cloneForInnerGoal(Infinity);
101
- try {
102
- for (const env of child.solve([premise], new Env(), 0)) {
103
- const resolvedPremise = copyResolved(premise, env);
104
- if (isAtomNamed(conclusion, 'false')) {
105
- const fuse = compound('fuse', [resolvedPremise]);
106
- onFuse(forwardLine(fuse, program, solver), fuse);
107
- haltCode = 2;
108
- break;
109
- }
110
- const line = forwardLine(resolvedPremise, program, solver);
111
- if (!seenAnswers.has(line)) {
112
- seenAnswers.add(line);
113
- onAnswer(line, resolvedPremise);
114
- }
115
- }
116
- } finally {
117
- solver.absorbStatsFrom(child);
118
- }
119
- if (haltCode != null) break;
81
+ const roundsVar = variable('\u0000eyeletRounds');
82
+ const derivedVar = variable('\u0000eyeletDerived');
83
+ const statusVar = variable('\u0000eyeletStatus');
84
+ const driver = compound('eyelet_run', [roundsVar, derivedVar, statusVar]);
85
+ driver.module = 'eyelet';
86
+
87
+ let resultEnv = null;
88
+ const previousEventHandler = solver.eyeletEventHandler;
89
+ solver.eyeletEventHandler = (kind, term) => {
90
+ if (kind === 'answer') {
91
+ onAnswer(forwardLine(term, program, solver), term);
92
+ } else if (kind === 'fuse') {
93
+ const fuse = compound('fuse', [term]);
94
+ onFuse(forwardLine(fuse, program, solver), fuse);
120
95
  }
121
- return { haltCode, rounds: 1, derived: 0 };
96
+ };
97
+ const iterator = solver.solve([driver], new Env(), 0);
98
+ try {
99
+ const result = iterator.next();
100
+ if (!result.done) resultEnv = result.value;
101
+ } finally {
102
+ try { iterator.return?.(); } catch (_) { /* best effort */ }
103
+ solver.eyeletEventHandler = previousEventHandler;
122
104
  }
105
+ if (resultEnv == null) return { haltCode: null, rounds: 0, derived: 0 };
123
106
 
124
- while (true) {
125
- rounds++;
126
- const conc = variable(`\u0000forwardConc${rounds}`);
127
- const prem = variable(`\u0000forwardPrem${rounds}`);
128
- const selector = compound(':+', [conc, prem]);
129
- const invocation = compound('call', [prem]);
130
- let changed = false;
131
-
132
- const child = solver.cloneForInnerGoal(Infinity);
133
- try {
134
- for (const env of child.solve([selector, invocation], new Env(), 0)) {
135
- const resolvedPremise = copyResolved(prem, env);
136
- let resolvedConclusion = copyResolved(conc, env);
137
-
138
- if (isAtomNamed(resolvedConclusion, 'true')) {
139
- const line = forwardLine(resolvedPremise, program, solver);
140
- if (!seenAnswers.has(line)) {
141
- seenAnswers.add(line);
142
- onAnswer(line, resolvedPremise);
143
- }
144
- continue;
145
- }
146
-
147
- if (isAtomNamed(resolvedConclusion, 'false')) {
148
- const fuse = compound('fuse', [resolvedPremise]);
149
- const line = forwardLine(fuse, program, solver);
150
- onFuse(line, fuse);
151
- haltCode = 2;
152
- break;
153
- }
154
-
155
- // A derived rule keeps its variables universally quantified. Ordinary
156
- // derived facts use Eyelet's historical existential convention: any
157
- // conclusion-only variables become sk_0, sk_1, ... before assertion.
158
- if (!isForwardRuleTerm(resolvedConclusion)) {
159
- resolvedConclusion = skolemizeForwardConclusion(resolvedConclusion);
160
- }
161
-
162
- for (const fact of forwardConjuncts(resolvedConclusion)) {
163
- if (isAtomNamed(fact, 'true') || isAtomNamed(fact, 'false')) continue;
164
- makePredicateDynamic(program, fact);
165
- if (!goalHasSolution(solver, fact)) {
166
- insertForwardFact(program, fact);
167
- derived++;
168
- changed = true;
169
- }
170
- }
171
- }
172
- } finally {
173
- solver.absorbStatsFrom(child);
174
- }
175
-
176
- if (haltCode != null) break;
177
-
178
- if (changed) continue;
179
-
180
- const closure = singletonInteger(program, 'closure', 0);
181
- const limit = singletonInteger(program, 'limit', -1);
182
- if (closure < limit) {
183
- setSingletonFact(program, 'closure', [numberTerm(closure + 1)]);
184
- continue;
185
- }
186
- break;
187
- }
107
+ const rounds = resolvedInteger(roundsVar, resultEnv, 0);
108
+ const derived = resolvedInteger(derivedVar, resultEnv, 0);
109
+ const status = copyResolved(statusVar, resultEnv);
110
+ const haltCode = status?.type === ATOM && status.name === 'fuse' ? 2 : null;
188
111
 
112
+ // Kept for source compatibility with the existing callback shape. The
113
+ // Prolog driver currently has no diagnostic event kind.
114
+ void onDiagnostic;
189
115
  return { haltCode, rounds, derived };
190
116
  }
191
117
 
192
-
193
- function forwardGroupIsPlainQueries(group) {
194
- return group.clauses.length > 0 && group.clauses.every((clause) => {
195
- if ((clause.body?.length ?? 0) !== 0) return false;
196
- const head = clause.head;
197
- if (head?.type !== COMPOUND || head.name !== ':+' || head.arity !== 2) return false;
198
- return isAtomNamed(head.args[0], 'true') || isAtomNamed(head.args[0], 'false');
199
- });
118
+ function resolvedInteger(term, env, fallback) {
119
+ const value = copyResolved(term, env);
120
+ return value?.type === NUMBER && /^-?\d+$/.test(value.name) ? Number(value.name) : fallback;
200
121
  }
201
122
 
202
123
  function forwardLine(term, program, solver) {
203
124
  return `${formatTermForWrite(term, new Env(), {
204
- // Eyelet historically used portray_clause/2. Keep character lists as
205
- // list syntax here rather than collapsing them to strings according to
206
- // the ambient double_quotes flag.
125
+ // Eyelet historically used portray_clause/2. Keep character lists as list
126
+ // syntax rather than collapsing them according to double_quotes.
207
127
  doubleQuotes: null,
208
128
  operators: [...program.operators.values()],
209
129
  quoted: true,
210
130
  compact: true,
211
131
  minimalOperatorSpacing: true,
212
- // Match portray_clause/2 more closely for Eyelet compatibility: current
213
- // operator atoms are legal unquoted arguments/list elements, and anonymous
214
- // source variables should be rendered with stable generated names rather
215
- // than their internal parser identifiers.
216
132
  operatorAtomsAsArgs: true,
217
133
  generateVariableNames: true,
218
134
  })}.\n`;
219
135
  }
220
-
221
- function isAtomNamed(term, name) {
222
- return term?.type === ATOM && term.name === name;
223
- }
224
-
225
- function isForwardRuleTerm(term) {
226
- return term?.type === COMPOUND && term.name === ':+' && term.arity === 2;
227
- }
228
-
229
- function forwardConjuncts(term) {
230
- if (isAtomNamed(term, 'true') || isAtomNamed(term, 'false')) return [];
231
- return flattenConjunction(term);
232
- }
233
-
234
- function skolemizeForwardConclusion(term) {
235
- const variables = new Map();
236
- const copy = (current) => {
237
- if (current.type === VAR) {
238
- let skolem = variables.get(current.name);
239
- if (skolem == null) {
240
- skolem = atom(`sk_${variables.size}`);
241
- variables.set(current.name, skolem);
242
- }
243
- return skolem;
244
- }
245
- if (current.type !== COMPOUND) return current;
246
- const result = compound(current.name, current.args.map(copy));
247
- if (current.module != null) result.module = current.module;
248
- return result;
249
- };
250
- return copy(term);
251
- }
252
-
253
- function makeGroupDynamic(program, group) {
254
- group.dynamic = true;
255
- program.dynamicPredicates.add(modulePredicateKey(group.module ?? 'user', group.name, group.arity));
256
- program.mutable = true;
257
- }
258
-
259
- function makePredicateDynamic(program, term) {
260
- if (term?.type !== ATOM && term?.type !== COMPOUND) return;
261
- if (term.type === COMPOUND && term.name === ',' && term.arity === 2) {
262
- makePredicateDynamic(program, term.args[0]);
263
- makePredicateDynamic(program, term.args[1]);
264
- return;
265
- }
266
- if (isForwardRuleTerm(term)) {
267
- const group = program.findGroup(':+', 2, term.module ?? 'user') ?? program.ensureDynamicGroup(':+', 2, term.module ?? 'user');
268
- makeGroupDynamic(program, group);
269
- makePredicateDynamic(program, term.args[0]);
270
- return;
271
- }
272
- if (['true', 'false', '!'].includes(term.name)) return;
273
- const module = term.module ?? 'user';
274
- const group = program.findGroup(term.name, term.arity, module) ?? program.ensureDynamicGroup(term.name, term.arity, module);
275
- makeGroupDynamic(program, group);
276
- }
277
-
278
- function predeclareForwardTerms(program, solver, group) {
279
- for (const clause of group.clauses) {
280
- const head = clause.head;
281
- if (head?.type !== COMPOUND || head.name !== ':+' || head.arity !== 2) continue;
282
- // Conclusions may later be asserted, while premise-only predicates need an
283
- // empty procedure so a not-yet-derived relation fails rather than raising
284
- // existence_error(procedure), matching Eyelet's former dynify/1 step.
285
- predeclareConclusionTerm(program, solver, head.args[0]);
286
- predeclareUnknownTerm(program, solver, head.args[1]);
287
- for (const bodyGoal of clause.body ?? []) predeclareUnknownTerm(program, solver, bodyGoal);
288
- }
289
- }
290
-
291
- function predeclareConclusionTerm(program, solver, term) {
292
- if (term?.type === COMPOUND && term.name === ',' && term.arity === 2) {
293
- predeclareConclusionTerm(program, solver, term.args[0]);
294
- predeclareConclusionTerm(program, solver, term.args[1]);
295
- return;
296
- }
297
- if (isForwardRuleTerm(term)) {
298
- makePredicateDynamic(program, term);
299
- predeclareConclusionTerm(program, solver, term.args[0]);
300
- predeclareUnknownTerm(program, solver, term.args[1]);
301
- return;
302
- }
303
- if (term?.type === ATOM || term?.type === COMPOUND) makePredicateDynamic(program, term);
304
- }
305
-
306
- function predeclareUnknownTerm(program, solver, term) {
307
- if (term?.type !== ATOM && term?.type !== COMPOUND) return;
308
- if (term.type === COMPOUND) {
309
- for (const arg of term.args) predeclareUnknownTerm(program, solver, arg);
310
- }
311
- if (['true', 'false', '!'].includes(term.name)) return;
312
- if (solver.registry.get(term.name, term.arity) != null) return;
313
- if (program.findGroup(term.name, term.arity, term.module ?? 'user') != null) return;
314
- const group = program.ensureDynamicGroup(term.name, term.arity, term.module ?? 'user');
315
- makeGroupDynamic(program, group);
316
- }
317
-
318
- function goalHasSolution(solver, goal) {
319
- const child = solver.cloneForInnerGoal(1);
320
- const iterator = child.solve([goal], new Env(), 0);
321
- try {
322
- const result = iterator.next();
323
- return !result.done;
324
- } finally {
325
- try { iterator.return?.(); } catch (_) { /* best effort */ }
326
- solver.absorbStatsFrom(child);
327
- }
328
- }
329
-
330
- function insertForwardFact(program, fact) {
331
- makePredicateDynamic(program, fact);
332
- program.insertDynamicClause({
333
- head: fact,
334
- module: fact.module ?? 'user',
335
- body: [],
336
- }, false);
337
- }
338
-
339
- function setSingletonFact(program, name, args, keyed = false) {
340
- const module = 'user';
341
- let group = program.findGroup(name, args.length, module);
342
- if (group == null) group = program.ensureDynamicGroup(name, args.length, module);
343
- makeGroupDynamic(program, group);
344
-
345
- if (keyed && args.length > 0) {
346
- const key = args[0]?.name;
347
- for (const clause of [...group.clauses]) {
348
- if (clause.head?.args?.[0]?.name === key) program.removeDynamicClause(group, clause);
349
- }
350
- } else {
351
- for (const clause of [...group.clauses]) program.removeDynamicClause(group, clause);
352
- }
353
- program.insertDynamicClause({ head: compound(name, args), module, body: [] }, false);
354
- }
355
-
356
- function singletonInteger(program, name, fallback) {
357
- const group = program.findGroup(name, 1, 'user');
358
- if (group == null) return fallback;
359
- for (let i = group.clauses.length - 1; i >= 0; i--) {
360
- const arg = group.clauses[i].head?.args?.[0];
361
- if (arg?.type === NUMBER && /^-?\d+$/.test(arg.name)) return Number(arg.name);
362
- }
363
- return fallback;
364
- }
365
-
366
-
package/src/lib/eyelet.pl CHANGED
@@ -1,43 +1,292 @@
1
- % Eyelet forward-reasoning support for EyeProlog.
1
+ % Eyelet forward reasoning for EyeProlog.
2
2
  %
3
- % The :+/2 closure itself is executed natively by EyeProlog. This module owns
4
- % the two state helpers used by Eyelet programs and exports the :+ operator so
5
- % source files can opt into the complete surface with one use_module/1.
3
+ % The fixed-point driver lives here in Prolog. JavaScript only bootstraps this
4
+ % module and adapts the recorded answer/fuse events to the embedding callbacks.
6
5
 
7
- :- module(eyelet, [op(1200, xfx, :+), stable/1, becomes/2]).
6
+ :- module(eyelet, [
7
+ op(1200, xfx, :+),
8
+ stable/1,
9
+ becomes/2
10
+ ]).
8
11
 
9
- :- use_module(library(iso_ext), [forall/2]).
10
- :- use_module(library(lists), [member/2]).
12
+ :- use_module(library(iso_ext), [variant/2]).
11
13
 
12
14
  :- meta_predicate(becomes(0, 0)).
13
15
 
16
+ :- dynamic(eyelet_reported/1).
17
+ :- dynamic(eyelet_changed/0).
18
+ :- dynamic(eyelet_fused/0).
19
+ :- dynamic(eyelet_derived/1).
20
+
21
+ % eyelet_run(-Rounds, -Derived, -Status)
22
+ % Run the user module's :+/2 rules to a fixed point. Status is ok or fuse.
23
+ % Answer/fuse terms are emitted through a tiny host callback adapter.
24
+ eyelet_run(Rounds, Derived, Status) :-
25
+ eyelet_setup,
26
+ ( plain_query_program
27
+ -> run_plain_queries,
28
+ Rounds = 1
29
+ ; eyelet_loop(0, Rounds)
30
+ ),
31
+ ( eyelet_fused -> Status = fuse ; Status = ok ),
32
+ eyelet_derived(Derived).
33
+
34
+ % Preparation is structural: inspect clauses instead of executing their bodies.
35
+ % dynify/1 also makes existing user predicates mutable, which is required by
36
+ % conclusions and becomes/2, while leaving imported and builtin predicates alone.
37
+ eyelet_setup :-
38
+ retractall(eyelet_reported(_)),
39
+ retractall(eyelet_changed),
40
+ retractall(eyelet_fused),
41
+ retractall(eyelet_derived(_)),
42
+ assertz(eyelet_derived(0)),
43
+ user:eyeprolog__dynify((:+)/2),
44
+ user:eyeprolog__dynify(closure/1),
45
+ user:eyeprolog__dynify(limit/1),
46
+ user:retractall(closure(_)),
47
+ user:retractall(limit(_)),
48
+ user:assertz(closure(0)),
49
+ user:assertz(limit(-1)),
50
+ prepare_rules.
51
+
52
+ prepare_rules :-
53
+ ( user:clause((Conc :+ Prem), Body),
54
+ dynify_goal(Conc),
55
+ dynify_goal(Prem),
56
+ dynify_goal(Body),
57
+ fail
58
+ ; true
59
+ ).
60
+
61
+ % Query-only Eyelet files are common. Detect them from clause heads and execute
62
+ % their premises once instead of entering the fixed-point loop.
63
+ plain_query_program :-
64
+ \+ ( user:clause((Conc :+ _), _), Conc \== true, Conc \== false ).
65
+
66
+ run_plain_queries :-
67
+ ( user:(Conc :+ Prem),
68
+ user:Prem,
69
+ process_control_conclusion(Conc, Prem, 1),
70
+ ( eyelet_fused -> ! ; fail )
71
+ ; true
72
+ ).
73
+
74
+ % Iterate until a round adds no new conclusion. stable/1 can request additional
75
+ % closure levels; each requested level advances only after a quiescent round.
76
+ eyelet_loop(Round0, Round) :-
77
+ Round1 is Round0 + 1,
78
+ retractall(eyelet_changed),
79
+ run_round(Round1),
80
+ ( eyelet_fused
81
+ -> Round = Round1
82
+ ; eyelet_changed
83
+ -> eyelet_loop(Round1, Round)
84
+ ; advance_closure
85
+ -> eyelet_loop(Round1, Round)
86
+ ; Round = Round1
87
+ ).
88
+
89
+ run_round(Round) :-
90
+ ( user:(Conc :+ Prem),
91
+ user:Prem,
92
+ process_conclusion(Conc, Prem, Round),
93
+ ( eyelet_fused -> ! ; fail )
94
+ ; true
95
+ ).
96
+
97
+ process_conclusion(Conc, Prem, Round) :-
98
+ ( Conc == true
99
+ -> report_answer(Prem)
100
+ ; Conc == false
101
+ -> report_fuse(Prem),
102
+ mark_fused
103
+ ; prepare_conclusion(Conc, Prepared),
104
+ assert_conj(Prepared)
105
+ ).
106
+
107
+ process_control_conclusion(Conc, Prem, _) :-
108
+ ( Conc == true
109
+ -> report_answer(Prem)
110
+ ; Conc == false
111
+ -> report_fuse(Prem),
112
+ mark_fused
113
+ ; true
114
+ ).
115
+
116
+ report_answer(Prem) :-
117
+ ( already_reported(Prem)
118
+ -> true
119
+ ; copy_term(Prem, Copy),
120
+ assertz(eyelet_reported(Copy)),
121
+ user:eyeprolog__eyelet_emit(answer, Prem)
122
+ ).
123
+
124
+ % Ground answers use the dynamic predicate's ground index. Only residual-variable
125
+ % answers need the more expensive variant scan.
126
+ already_reported(Prem) :-
127
+ term_variables(Prem, Vars),
128
+ ( Vars == []
129
+ -> eyelet_reported(Prem)
130
+ ; eyelet_reported(Seen),
131
+ variant(Prem, Seen)
132
+ ),
133
+ !.
134
+
135
+ report_fuse(Prem) :-
136
+ user:eyeprolog__eyelet_emit(fuse, Prem).
137
+
138
+ mark_fused :-
139
+ ( eyelet_fused -> true ; assertz(eyelet_fused) ).
140
+
141
+ prepare_conclusion(Conc, Conc) :-
142
+ is_forward_rule(Conc),
143
+ !.
144
+ prepare_conclusion(Conc, Conc) :-
145
+ skolemize(Conc, 0, _).
146
+
147
+ is_forward_rule(Term) :-
148
+ nonvar(Term),
149
+ Term = (_ :+ _).
150
+
151
+ % Assert each new conjunct. Ordinary conclusions are considered already known
152
+ % when they are provable; a derived :+/2 rule is considered known only when an
153
+ % identical fact-clause already exists, so guarded source rules are not confused
154
+ % with a newly derived unconditional rule.
155
+ assert_conj(true) :- !.
156
+ assert_conj(false) :- !.
157
+ assert_conj((A, B)) :-
158
+ !,
159
+ assert_conj(A),
160
+ assert_conj(B).
161
+ assert_conj(Goal) :-
162
+ dynify_goal(Goal),
163
+ ( known_conclusion(Goal)
164
+ -> true
165
+ ; user:assertz(Goal),
166
+ mark_changed
167
+ ).
168
+
169
+ known_conclusion(Goal) :-
170
+ is_forward_rule(Goal),
171
+ !,
172
+ user:clause(Goal, true).
173
+ known_conclusion(Goal) :-
174
+ user:Goal.
175
+
176
+ mark_changed :-
177
+ ( eyelet_changed -> true ; assertz(eyelet_changed) ),
178
+ retract(eyelet_derived(N)),
179
+ N1 is N + 1,
180
+ assertz(eyelet_derived(N1)).
181
+
182
+ advance_closure :-
183
+ user:closure(Closure),
184
+ user:limit(Limit),
185
+ Closure < Limit,
186
+ NewClosure is Closure + 1,
187
+ user:retract(closure(Closure)),
188
+ user:assertz(closure(NewClosure)).
189
+
190
+ % skolemize(+Term, +N0, -N)
191
+ % Replace remaining conclusion variables with sk_0, sk_1, ... . Derived :+/2
192
+ % rules skip this step so their variables remain universally quantified.
193
+ skolemize(Term, N0, N) :-
194
+ term_variables(Term, Vars),
195
+ skolemize_vars(Vars, N0, N).
196
+
197
+ skolemize_vars([], N, N).
198
+ skolemize_vars([Sk|Vars], N0, N) :-
199
+ number_chars(N0, Digits),
200
+ atom_chars(Number, Digits),
201
+ atom_concat(sk_, Number, Sk),
202
+ N1 is N0 + 1,
203
+ skolemize_vars(Vars, N1, N).
204
+
14
205
  % stable(+Level)
15
- % Fail until the native forward-rule driver has reached the requested closure
16
- % level. Asking for a higher level extends the driver's target limit.
206
+ % Fail until the requested closure level has been reached. Asking for a higher
207
+ % level raises the target; the driver advances it only at a quiescent round.
17
208
  stable(Level) :-
18
- call(user:limit(Limit)),
209
+ user:limit(Limit),
19
210
  ( Limit < Level
20
211
  -> user:retract(limit(Limit)),
21
212
  user:assertz(limit(Level))
22
213
  ; true
23
214
  ),
24
- call(user:closure(Closure)),
215
+ user:closure(Closure),
25
216
  Level =< Closure.
26
217
 
27
218
  % becomes(:From, :To)
28
- % Linear implication over dynamic state: prove and retract every conjunct in
29
- % From, then assert every conjunct in To.
219
+ % Linear implication over dynamic state. dynify/1 first makes existing source
220
+ % predicates mutable, so callers no longer need a separate dynamic/1 directive.
30
221
  becomes(From, To) :-
31
- catch(call(user:From), _, fail),
222
+ dynify_goal(From),
223
+ dynify_goal(To),
224
+ catch(user:From, _, fail),
32
225
  conj_list(From, Old),
33
- forall(member(Clause, Old), user:retract(Clause)),
226
+ retract_conj(Old),
34
227
  conj_list(To, New),
35
- forall(member(Clause, New), user:assertz(Clause)).
228
+ assert_conj_list(New).
36
229
 
37
- conj_list(true, []).
38
- conj_list(Goal, [Goal]) :-
39
- Goal \= (_, _),
40
- Goal \= false,
230
+ retract_conj([]).
231
+ retract_conj([Clause|Clauses]) :-
232
+ user:retract(Clause),
233
+ retract_conj(Clauses).
234
+
235
+ assert_conj_list([]).
236
+ assert_conj_list([Clause|Clauses]) :-
237
+ user:assertz(Clause),
238
+ assert_conj_list(Clauses).
239
+
240
+ % Flatten conjunctions in either association direction without append/3.
241
+ conj_list(Goal, List) :-
242
+ conj_list(Goal, List, []).
243
+
244
+ conj_list(true, Tail, Tail) :- !.
245
+ conj_list(false, _, _) :- !, fail.
246
+ conj_list((A, B), List, Tail) :-
247
+ !,
248
+ conj_list(A, List, Rest),
249
+ conj_list(B, Rest, Tail).
250
+ conj_list(Goal, [Goal|Tail], Tail).
251
+
252
+ % Prepare a callable goal and recursively inspect compound arguments. Atomic
253
+ % arguments are data, but an atom in callable position denotes a 0-arity
254
+ % predicate and therefore needs an empty dynamic procedure when it is unknown.
255
+ dynify_goal(Term) :-
256
+ var(Term),
257
+ !.
258
+ dynify_goal(Term) :-
259
+ atom(Term),
260
+ !,
261
+ user:eyeprolog__dynify(Term/0).
262
+ dynify_goal(Term) :-
263
+ atomic(Term),
41
264
  !.
42
- conj_list((A, B), [A|Rest]) :-
43
- conj_list(B, Rest).
265
+ dynify_goal(Term) :-
266
+ dynify_term(Term).
267
+
268
+ % Recursively prepare callable shapes embedded in compound arguments. The
269
+ % private host primitive only mutates direct user procedures; imports, builtins,
270
+ % and control constructs are left untouched.
271
+ dynify_term(Term) :-
272
+ var(Term),
273
+ !.
274
+ dynify_term(Term) :-
275
+ atomic(Term),
276
+ !.
277
+ dynify_term([]) :-
278
+ !.
279
+ dynify_term([Head|Tail]) :-
280
+ !,
281
+ dynify_term(Head),
282
+ dynify_term(Tail).
283
+ dynify_term(Term) :-
284
+ functor(Term, Name, Arity),
285
+ user:eyeprolog__dynify(Name/Arity),
286
+ Term =.. [_|Args],
287
+ dynify_list(Args).
288
+
289
+ dynify_list([]).
290
+ dynify_list([Term|Terms]) :-
291
+ dynify_term(Term),
292
+ dynify_list(Terms).
package/src/parser.js CHANGED
@@ -92,10 +92,7 @@ const graphicAtomChars = '#$&*+-./<=>?@^~\\:';
92
92
  // canonical notation. Commas remain separators except inside parentheses.
93
93
  const INFIX_OPERATORS = new Map([
94
94
  [':-', { precedence: 1, associativity: 'none' }],
95
- [':+', { precedence: 1, associativity: 'none' }], // EyeProlog forward-rule extension
96
- ['?-', { precedence: 1, associativity: 'none' }], // quad label extension
97
95
  ['-->', { precedence: 1, associativity: 'none' }],
98
- ['|', { precedence: 96, associativity: 'right' }],
99
96
  [';', { precedence: 101, associativity: 'right' }],
100
97
  ['->', { precedence: 151, associativity: 'right' }],
101
98
  [',', { precedence: 201, associativity: 'right' }],
@@ -133,7 +130,6 @@ const INFIX_OPERATORS = new Map([
133
130
  ]);
134
131
  const PREFIX_OPERATORS = new Map([
135
132
  ['?-', { precedence: 1, strict: true }],
136
- ['table', { precedence: 51, strict: true }],
137
133
  ['\\+', { precedence: 301, strict: false }],
138
134
  ['+', { precedence: 1001, strict: false }],
139
135
  ['-', { precedence: 1001, strict: false }],
@@ -159,7 +155,7 @@ export const ISO_OPERATOR_DEFINITIONS = [
159
155
  export const PART3_OPERATOR_DEFINITIONS = [
160
156
  [1105, 'xfy', '|'],
161
157
  // EyeProlog forward-rule extension. A top-level Conclusion :+ Premise is
162
- // executed by the native closure driver when no explicit CLI goal is given.
158
+ // executed by library(eyelet)'s Prolog closure driver when no explicit CLI goal is given.
163
159
  [1200, 'xfx', ':+'],
164
160
  // Tabling is explicit in the normal EyeProlog profile. Make the common
165
161
  // `:- table p/n.` declaration available without requiring a library import.
@@ -229,14 +225,10 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
229
225
  prefixOperators: includeDefaults ? new Map(PREFIX_OPERATORS) : new Map(),
230
226
  postfixOperators: new Map(),
231
227
  };
232
- // The infix ?-/2 form is an EyeProlog quad extension. ISO 13211-1
233
- // predefines only the 1200 fx ?- operator; strict core mode starts from that
234
- // table and still permits an explicit op/3 directive to add an infix form.
235
- if (options.isoStrict === true) {
236
- state.infixOperators.delete('?-');
237
- state.infixOperators.delete('|');
238
- state.infixOperators.delete(':+');
239
- state.prefixOperators.delete('table');
228
+ if (includeDefaults && options.isoStrict !== true) {
229
+ for (const [priority, specifier, name] of [...PART3_OPERATOR_DEFINITIONS, ...QUAD_OPERATOR_DEFINITIONS]) {
230
+ defineParserOperator(state, priority, specifier, name);
231
+ }
240
232
  }
241
233
  for (const definition of definitions) {
242
234
  const [priority, specifier, name] = Array.isArray(definition)
package/src/program.js CHANGED
@@ -65,22 +65,12 @@ function collectMultiplicitySensitiveDependencies(goal, out = []) {
65
65
  }
66
66
 
67
67
 
68
- function scalarNodeKey(term) {
69
- return isScalar(term) ? `${term.type}:${term.name}` : null;
70
- }
71
-
72
- // Some source-level self recursion is structurally finite even though the
73
- // predicate dependency graph contains a self-edge. The deep-taxonomy family
74
- // is the canonical case: p(X,n10000) calls p(X,n9999), eventually reaching a
75
- // fact. When one scalar argument forms an acyclic transition graph and every
76
- // other argument is carried through unchanged, depth-first execution is both
77
- // terminating and much cheaper than constructing the complete Datalog model.
78
68
  // Cut-guided tail recursion is usually a deterministic search loop rather than
79
- // a cyclic relation. Tabling such loops can retain every intermediate numeric
69
+ // a cyclic relation. Tabling such loops can retain every intermediate numeric
80
70
  // state (for example prime scanning in Goldbach) and turn a bounded depth-first
81
- // search into an enormous table. Keep this deliberately narrow: each recursive
71
+ // search into an enormous table. Keep this deliberately narrow: each recursive
82
72
  // clause must contain exactly one direct self-call and that call must be the last
83
- // body goal. Branching recursion such as Takeuchi therefore remains eligible
73
+ // body goal. Branching recursion such as Takeuchi therefore remains eligible
84
74
  // for recursion planning and explicit tabling.
85
75
  function hasDirectCutTailRecursion(group) {
86
76
  if (group.hasCut !== true || group.clauses.length === 0) return false;
@@ -104,96 +94,6 @@ function hasDirectCutTailRecursion(group) {
104
94
  return sawRecursive;
105
95
  }
106
96
 
107
- function hasAcyclicScalarSelfRecursion(group) {
108
- if (group.arity === 0 || group.clauses.length === 0) return false;
109
- for (let position = 0; position < group.arity; position++) {
110
- const edges = new Map();
111
- let sawRecursive = false;
112
- let valid = true;
113
- for (const clause of group.clauses) {
114
- if (isCompactBinaryClause(clause)) {
115
- if (group.arity !== 2) { valid = false; break; }
116
- if (clause.bodyName == null) continue;
117
- if (clause.bodyName !== group.name) { valid = false; break; }
118
- const headType = position === 0 ? clause.head0Type : clause.head1Type;
119
- const headName = position === 0 ? clause.head0Name : clause.head1Name;
120
- const bodyType = position === 0 ? clause.body0Type : clause.body1Type;
121
- const bodyName = position === 0 ? clause.body0Name : clause.body1Name;
122
- if (!['atom', 'string', 'number'].includes(headType) ||
123
- !['atom', 'string', 'number'].includes(bodyType)) { valid = false; break; }
124
- const other = position === 0 ? 1 : 0;
125
- const ohType = other === 0 ? clause.head0Type : clause.head1Type;
126
- const ohName = other === 0 ? clause.head0Name : clause.head1Name;
127
- const obType = other === 0 ? clause.body0Type : clause.body1Type;
128
- const obName = other === 0 ? clause.body0Name : clause.body1Name;
129
- const carried = (ohType === 'var' && obType === 'var' && ohName === obName) ||
130
- (['atom', 'string', 'number'].includes(ohType) && ohType === obType && ohName === obName);
131
- if (!carried) { valid = false; break; }
132
- const from = `${headType}:${headName}`;
133
- const to = `${bodyType}:${bodyName}`;
134
- sawRecursive = true;
135
- let targets = edges.get(from);
136
- if (targets == null) edges.set(from, targets = new Set());
137
- targets.add(to);
138
- continue;
139
- }
140
-
141
- if (clause.head?.type !== COMPOUND || clause.head.name !== group.name || clause.head.arity !== group.arity) {
142
- valid = false; break;
143
- }
144
- if ((clause.body?.length ?? 0) === 0) continue;
145
- if (clause.body.length !== 1) { valid = false; break; }
146
- const body = clause.body[0];
147
- if (body?.type !== COMPOUND || body.name !== group.name || body.arity !== group.arity ||
148
- (body.module ?? group.module) !== group.module) { valid = false; break; }
149
- const from = scalarNodeKey(clause.head.args[position]);
150
- const to = scalarNodeKey(body.args[position]);
151
- if (from == null || to == null) { valid = false; break; }
152
- for (let i = 0; i < group.arity; i++) {
153
- if (i === position) continue;
154
- const h = clause.head.args[i];
155
- const b = body.args[i];
156
- if (h.type === VAR && b.type === VAR && h.name === b.name) continue;
157
- const hk = scalarNodeKey(h);
158
- const bk = scalarNodeKey(b);
159
- if (hk != null && hk === bk) continue;
160
- valid = false;
161
- break;
162
- }
163
- if (!valid) break;
164
- sawRecursive = true;
165
- let targets = edges.get(from);
166
- if (targets == null) edges.set(from, targets = new Set());
167
- targets.add(to);
168
- }
169
- if (!valid || !sawRecursive) continue;
170
-
171
- // Use Kahn's algorithm rather than recursive DFS here. Programs generated
172
- // from large taxonomies can contain hundreds of thousands of scalar
173
- // transitions, and recursing once per edge would overflow the JavaScript
174
- // call stack before Prolog execution even begins.
175
- const indegree = new Map();
176
- for (const [node, targets] of edges) {
177
- if (!indegree.has(node)) indegree.set(node, 0);
178
- for (const next of targets) indegree.set(next, (indegree.get(next) ?? 0) + 1);
179
- }
180
- const ready = [];
181
- for (const [node, degree] of indegree) if (degree === 0) ready.push(node);
182
- let consumed = 0;
183
- for (let cursor = 0; cursor < ready.length; cursor++) {
184
- const node = ready[cursor];
185
- consumed++;
186
- for (const next of edges.get(node) ?? []) {
187
- const degree = indegree.get(next) - 1;
188
- indegree.set(next, degree);
189
- if (degree === 0) ready.push(next);
190
- }
191
- }
192
- if (consumed === indegree.size) return true;
193
- }
194
- return false;
195
- }
196
-
197
97
  function preparedBundledLibraryCacheKey(program, options) {
198
98
  const filename = String(options.filename ?? '');
199
99
  if (filename !== standardLibrarySources.get('clpz')?.filename) return null;
@@ -562,10 +462,8 @@ export class Program {
562
462
  );
563
463
  group.listTailRecursive = directRecursiveComponent && !group.cutRecursive &&
564
464
  hasStrictListTailRecursion(group);
565
- const acyclicScalarRecursion = plannedRecursive && hasAcyclicScalarSelfRecursion(group);
566
465
  const cutTailRecursion = plannedRecursive && hasDirectCutTailRecursion(group);
567
466
  const multiplicitySensitive = [...multiplicitySensitiveRoots].some((root) => reachableIndexes(root, deps).has(start));
568
- group.acyclicScalarRecursion = acyclicScalarRecursion;
569
467
  group.cutTailRecursion = cutTailRecursion;
570
468
  group.multiplicitySensitive = multiplicitySensitive;
571
469
  group.depthFirstRecursive = plannedRecursive && !explicitlyTabled;
@@ -1030,6 +928,43 @@ function hybridFastSource(source, options) {
1030
928
  // that phase, so extend the existing instance with the same canonical interop
1031
929
  // imports before solving those goals. Keeping this here lets both paths share
1032
930
  // the dependency analysis and module-loading rules.
931
+ // Load one bundled library module into an already-built Program without
932
+ // importing its public predicates into user. This is useful for host bootstraps
933
+ // that call a module-private Prolog entry point while keeping the user-facing
934
+ // module export list unchanged.
935
+ export function loadBundledProgramLibrary(program, name, options = {}) {
936
+ if (program.modules.has(name)) return program;
937
+ const operatorState = createParserOperatorState(
938
+ [...program.operators.values()],
939
+ false,
940
+ { isoStrict: program.strictIso },
941
+ );
942
+ const parserFlagState = {
943
+ doubleQuotes: program.doubleQuotes ?? options.doubleQuotes ?? 'chars',
944
+ charConversion: 'on',
945
+ charConversions: new Map(),
946
+ };
947
+ const builder = new ProgramBuilder({ ...options, isoStrict: program.strictIso }, program);
948
+ const loadedModules = new Set([...program.modules.keys()].filter((module) => module !== 'user'));
949
+ const ensured = new Set();
950
+ const designation = compound('library', [atom(name)]);
951
+ const loaded = readModuleSource(designation, {
952
+ ...options,
953
+ isoStrict: program.strictIso,
954
+ operatorState,
955
+ parserFlagState,
956
+ });
957
+ const childContext = { module: loaded.name };
958
+ if (!loadSourceIntoBuilder(
959
+ builder, loaded.text, loaded.options, ensured, loadedModules, false, childContext,
960
+ )) {
961
+ throw new Error(`could not load library(${name})`);
962
+ }
963
+ program.noteMutation(false);
964
+ builder.finish();
965
+ return program;
966
+ }
967
+
1033
968
  export function autoloadProgramGoals(program, inputs, options = {}) {
1034
969
  if (inputs == null || (Array.isArray(inputs) && inputs.length === 0)) return program;
1035
970
  const operatorState = createParserOperatorState(
@@ -1131,13 +1066,15 @@ function bundledLibraryModule(program, module) {
1131
1066
  function groupDependencies(group) {
1132
1067
  const dependencies = [];
1133
1068
  for (const clause of group.clauses) {
1134
- // Native forward rules store their executable premise in the :+/2 head
1135
- // rather than in the ordinary clause body. Treat that premise as a goal
1136
- // dependency so bundled predicates such as stable/1 and becomes/2 can be
1137
- // autoloaded exactly like calls in ordinary rules. Derived :+ rules may
1138
- // themselves occur in a conclusion, so recurse through that shape too.
1069
+ // Forward rules store executable premises inside their heads instead of in
1070
+ // ordinary clause bodies. Include those premises in static dependency
1071
+ // analysis so portability checks see the same calls that runtime will see.
1139
1072
  if (group.name === ':+') {
1140
- dependencies.push(...forwardRuleDependencies(clause.head));
1073
+ collectForwardRulePremiseDependencies(
1074
+ clause.head,
1075
+ (goal, out) => out.push(...collectGoalDependencies(goal, false, true)),
1076
+ dependencies,
1077
+ );
1141
1078
  }
1142
1079
  if (isCompactBinaryClause(clause)) {
1143
1080
  if (clause.bodyName != null) {
@@ -1157,16 +1094,16 @@ function groupDependencies(group) {
1157
1094
  return dependencies;
1158
1095
  }
1159
1096
 
1160
- function forwardRuleDependencies(term, out = []) {
1097
+ function collectForwardRulePremiseDependencies(term, collect, out) {
1161
1098
  if (term?.type !== COMPOUND) return out;
1162
1099
  if (term.name === ':+' && term.arity === 2) {
1163
- out.push(...collectGoalDependencies(term.args[1], false, true));
1164
- forwardRuleDependencies(term.args[0], out);
1100
+ collect(term.args[1], out);
1101
+ collectForwardRulePremiseDependencies(term.args[0], collect, out);
1165
1102
  return out;
1166
1103
  }
1167
1104
  if (term.name === ',' && term.arity === 2) {
1168
- forwardRuleDependencies(term.args[0], out);
1169
- forwardRuleDependencies(term.args[1], out);
1105
+ collectForwardRulePremiseDependencies(term.args[0], collect, out);
1106
+ collectForwardRulePremiseDependencies(term.args[1], collect, out);
1170
1107
  }
1171
1108
  return out;
1172
1109
  }
@@ -1220,24 +1157,12 @@ function collectAutoloadGoalDependencies(goal, out = []) {
1220
1157
  return out;
1221
1158
  }
1222
1159
 
1223
- function forwardRuleAutoloadDependencies(term, out = []) {
1224
- if (term?.type !== COMPOUND) return out;
1225
- if (term.name === ':+' && term.arity === 2) {
1226
- collectAutoloadGoalDependencies(term.args[1], out);
1227
- forwardRuleAutoloadDependencies(term.args[0], out);
1228
- return out;
1229
- }
1230
- if (term.name === ',' && term.arity === 2) {
1231
- forwardRuleAutoloadDependencies(term.args[0], out);
1232
- forwardRuleAutoloadDependencies(term.args[1], out);
1233
- }
1234
- return out;
1235
- }
1236
-
1237
1160
  function groupAutoloadDependencies(group) {
1238
1161
  const dependencies = [];
1239
1162
  for (const clause of group.clauses) {
1240
- if (group.name === ':+') dependencies.push(...forwardRuleAutoloadDependencies(clause.head));
1163
+ if (group.name === ':+') {
1164
+ collectForwardRulePremiseDependencies(clause.head, collectAutoloadGoalDependencies, dependencies);
1165
+ }
1241
1166
  if (isCompactBinaryClause(clause)) {
1242
1167
  if (clause.bodyName != null) {
1243
1168
  dependencies.push({
@@ -4,9 +4,77 @@
4
4
  // backtrackable blackboard lives in Env so ordinary Prolog choice points undo
5
5
  // writes automatically.
6
6
 
7
- import { ATOM, COMPOUND, NUMBER, STRING, VAR, atom, compound, deref, listFromItems, numberTerm, numberTextFromDouble, unify } from './term.js';
7
+ import { ATOM, COMPOUND, NUMBER, STRING, VAR, atom, compound, copyResolved, deref, listFromItems, numberTerm, numberTextFromDouble, unify } from './term.js';
8
8
  import { PrologError } from './errors.js';
9
9
  import { formatTermForWrite } from './write.js';
10
+ import { modulePredicateKey, rebuildGroupIndexes } from './program-indexing.js';
11
+
12
+
13
+ const NON_PROCEDURE_CONTROL = new Set([',/2', ';/2', '->/2', ':/2', '!/0', 'true/0', 'false/0', 'fail/0']);
14
+
15
+ function dynifyIndicator(term, env) {
16
+ const indicator = deref(term, env);
17
+ if (indicator.type === VAR) throw new PrologError('instantiation_error');
18
+ if (indicator.type !== COMPOUND || indicator.name !== '/' || indicator.arity !== 2) {
19
+ throw new PrologError('type_error(predicate_indicator)', indicator);
20
+ }
21
+ const name = deref(indicator.args[0], env);
22
+ const arity = deref(indicator.args[1], env);
23
+ if (name.type === VAR || arity.type === VAR) throw new PrologError('instantiation_error');
24
+ if (name.type !== ATOM || arity.type !== NUMBER || !/^\d+$/.test(arity.name)) {
25
+ throw new PrologError('type_error(predicate_indicator)', indicator);
26
+ }
27
+ const value = BigInt(arity.name);
28
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new PrologError('representation_error(max_arity)');
29
+ return { name: name.name, arity: Number(value), indicator };
30
+ }
31
+
32
+ // Private support for declarative libraries that need to prepare user predicates
33
+ // for database updates. Unlike assertz/retract probes, this can make an existing
34
+ // source predicate dynamic without changing its clauses, and it leaves imported
35
+ // or builtin procedures alone instead of accidentally shadowing them.
36
+ function* dynifyBuiltin({ solver, goal, env }) {
37
+ const target = dynifyIndicator(goal.args[0], env);
38
+ const module = goal.module ?? 'user';
39
+ const indicatorKey = `${target.name}/${target.arity}`;
40
+ if (NON_PROCEDURE_CONTROL.has(indicatorKey)) {
41
+ yield env;
42
+ return;
43
+ }
44
+
45
+ const directKey = modulePredicateKey(module, target.name, target.arity);
46
+ let group = solver.program.groups.get(directKey) ?? null;
47
+ if (group != null) {
48
+ if (!group.dynamic) {
49
+ group.dynamic = true;
50
+ solver.program.dynamicPredicates.add(directKey);
51
+ solver.program.mutable = true;
52
+ rebuildGroupIndexes(group);
53
+ solver.program.noteMutation(group.clauses.some((clause) => (clause.body?.length ?? 0) > 0));
54
+ }
55
+ yield env;
56
+ return;
57
+ }
58
+
59
+ if (solver.registry.get(target.name, target.arity) != null ||
60
+ solver.program.moduleImports.get(module)?.has(indicatorKey) === true) {
61
+ yield env;
62
+ return;
63
+ }
64
+
65
+ group = solver.program.ensureDynamicGroup(target.name, target.arity, module);
66
+ group.dynamic = true;
67
+ yield env;
68
+ }
69
+
70
+ function* eyeletEmitBuiltin({ solver, goal, env }) {
71
+ const kind = deref(goal.args[0], env);
72
+ if (kind.type === VAR) throw new PrologError('instantiation_error');
73
+ if (kind.type !== ATOM) throw new PrologError('type_error(atom)', kind);
74
+ const term = copyResolved(goal.args[1], env);
75
+ solver.eyeletEventHandler?.(kind.name, term);
76
+ yield env;
77
+ }
10
78
 
11
79
  function blackboardKey(term, env) {
12
80
  const value = deref(term, env);
@@ -255,6 +323,8 @@ function* portrayClauseBuiltin({ solver, goal, env }) {
255
323
 
256
324
  export const scryerCompatibilityBuiltins = {
257
325
  register(registry) {
326
+ registry.add('eyeprolog__dynify', 1, dynifyBuiltin, { deterministic: true, eyePrologLibrary: true });
327
+ registry.add('eyeprolog__eyelet_emit', 2, eyeletEmitBuiltin, { deterministic: true, eyePrologLibrary: true });
258
328
  registry.add('eyeprolog__bb_get', 2, bbGetBuiltin, { deterministic: true, eyePrologLibrary: true });
259
329
  registry.add('eyeprolog__bb_b_put', 2, bbPutBuiltin, { deterministic: true, eyePrologLibrary: true });
260
330
  registry.add('eyeprolog__bb_global_get', 2, bbGlobalGetBuiltin, { deterministic: true, eyePrologLibrary: true });
@@ -4806,7 +4806,7 @@ ${profile}`;
4806
4806
  assertIncludes(book, 'interactive top-level query therefore autoloads its canonical', 'REPL autoload');
4807
4807
  assertIncludes(book, 'Autoloading therefore supplies', 'autoload syntax boundary');
4808
4808
  assertIncludes(book, 'Residual constraints are part of the displayed answer even when', 'top-level hidden residuals');
4809
- assertIncludes(book, 'EyeProlog normal mode also accepts `:+`', 'native forward-rule extension');
4809
+ assertIncludes(book, 'EyeProlog normal mode also accepts `:+`', 'Eyelet forward-rule extension');
4810
4810
  assertIncludes(book, '`library(eyelet)`', 'Eyelet library surface');
4811
4811
  assertIncludes(readme, '`library(eyelet)`', 'README Eyelet library surface');
4812
4812
  assertIncludes(profile, 'an unresolved unqualified predicate may autoload its unique provider', 'Why EyeProlog autoload policy');
@@ -5464,7 +5464,7 @@ function apiCases() {
5464
5464
  },
5465
5465
  },
5466
5466
  {
5467
- name: 'run executes native forward rules when no explicit goal is supplied',
5467
+ name: 'run executes Prolog Eyelet forward rules when no explicit goal is supplied',
5468
5468
  run: () => {
5469
5469
  const result = run('seed(a).\nseen(X) :+ seed(X).\ntrue :+ seen(X).\n');
5470
5470
  assertEqual(result.stdout, 'seen(a).\n', 'forward stdout');
@@ -5472,10 +5472,9 @@ function apiCases() {
5472
5472
  },
5473
5473
  },
5474
5474
  {
5475
- name: 'native forward rules autoload library(eyelet) helpers from :+ premises',
5475
+ name: 'Eyelet forward rules autoload library helpers and dynify source state',
5476
5476
  run: () => {
5477
5477
  const result = run(`
5478
- :- dynamic(state/1).
5479
5478
  state(a).
5480
5479
  changed :+ becomes(state(a), state(b)).
5481
5480
  seed :+ state(b).
@@ -5485,6 +5484,21 @@ true :+ ready.
5485
5484
  assertEqual(result.stdout, 'ready.\n', 'forward helper stdout');
5486
5485
  },
5487
5486
  },
5487
+ {
5488
+ name: 'Eyelet false conclusions emit a fuse and halt status 2',
5489
+ run: () => {
5490
+ const result = run('bad(a).\nfalse :+ bad(X).\n');
5491
+ assertEqual(result.stdout, 'fuse(bad(a)).\n', 'fuse stdout');
5492
+ assertEqual(result.haltCode, 2, 'fuse halt code');
5493
+ },
5494
+ },
5495
+ {
5496
+ name: 'Eyelet ordinary conclusions skolemize conclusion-only variables',
5497
+ run: () => {
5498
+ const result = run('seed(a).\npair(X, Y) :+ seed(X).\ntrue :+ pair(X, Y).\n');
5499
+ assertEqual(result.stdout, 'pair(a,sk_0).\n', 'skolemized forward answer');
5500
+ },
5501
+ },
5488
5502
  {
5489
5503
  name: 'run exposes false/0 as an always-failing built-in',
5490
5504
  run: () => {
@@ -6283,7 +6297,11 @@ answer(ok) :-
6283
6297
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
6284
6298
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
6285
6299
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
6286
- assertEqual(library.defs.size, 163, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and generic library adapters');
6300
+ assertEqual(library.defs.size, 165, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and generic library adapters');
6301
+ assertEqual(registry.get('eyeprolog__dynify', 1), null, 'Eyelet dynify adapter is absent from the ISO registry');
6302
+ assertEqual(Boolean(library.get('eyeprolog__dynify', 1)), true, 'Eyelet dynify adapter is an internal EyeProlog library primitive');
6303
+ assertEqual(registry.get('eyeprolog__eyelet_emit', 2), null, 'Eyelet event adapter is absent from the ISO registry');
6304
+ assertEqual(Boolean(library.get('eyeprolog__eyelet_emit', 2)), true, 'Eyelet event adapter is an internal EyeProlog library primitive');
6287
6305
  assertEqual(registry.get('eyeprolog__random_value', 1), null, 'native random helper is absent from the ISO registry');
6288
6306
  assertEqual(Boolean(library.get('eyeprolog__random_value', 1)), true, 'native random helper is an internal EyeProlog library adapter');
6289
6307
  assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
@@ -176,7 +176,7 @@ This rhythm deliberately joins declarative reading, operational reading, and
176
176
  program construction. Readers new to logic programming can follow Parts I–III
177
177
  in order. Experienced Prolog programmers can begin with Chapters 3, 13, and
178
178
  17 to see how EyeProlog combines ordinary depth-first Prolog with explicit
179
- tabling, native forward rules, and inspectable proofs.
179
+ tabling, Eyelet forward rules, and inspectable proofs.
180
180
  Chapter 41 gives further routes through the material.
181
181
 
182
182
  ### When a run surprises you
@@ -1668,7 +1668,7 @@ list, one by finitely many tabled graph answers, and one that constructs terms
1668
1668
  without bound. State why the first two may terminate and why tabling does not
1669
1669
  repair the third.
1670
1670
 
1671
- ### Native forward rules and Eyelet execution
1671
+ ### Eyelet forward rules and Prolog execution
1672
1672
 
1673
1673
  EyeProlog normal mode also accepts `:+` at priority 1200 as an `xfx` operator.
1674
1674
  A source term
@@ -1678,19 +1678,34 @@ Conclusion :+ Premise.
1678
1678
  ```
1679
1679
 
1680
1680
  is a forward rule. When a loaded program contains such rules and no explicit
1681
- top-level goal overrides them, the engine runs a native closure driver rather
1682
- than a Prolog meta-interpreter. It repeatedly solves premises against the
1683
- current program, asserts novel conjuncts from successful conclusions, and
1684
- continues until no new conclusion is added. This is the execution path used when
1685
- Eyelet sources are run directly by EyeProlog.
1681
+ top-level goal overrides them, the engine loads `library(eyelet)` and invokes its
1682
+ Prolog closure driver. The driver inspects `:+/2` clauses structurally, prepares
1683
+ state predicates for updates, repeatedly solves premises against the current
1684
+ program, asserts novel conjuncts from successful conclusions, and continues until
1685
+ no new conclusion is added. Query-only programs take a single-pass fast path
1686
+ because they cannot grow the closure. This is the execution path used when Eyelet
1687
+ sources are run directly by EyeProlog.
1686
1688
 
1687
1689
  Two conclusions have control meaning. `true :+ Goal` is a query and prints each
1688
1690
  distinct successful instance of `Goal`. `false :+ Goal` is an integrity fuse:
1689
1691
  on success EyeProlog prints `fuse(Goal)` and returns halt status 2. Variables
1690
1692
  that occur only in an ordinary derived conclusion are existential and become
1691
1693
  `sk_0`, `sk_1`, and so on; a derived conclusion that is itself a `:+` rule keeps
1692
- its variables universal. The bundled `library(eyelet)` exports the `:+` operator together with
1693
- `stable/1` and `becomes/2`, while closure selection and assertion are native.
1694
+ its variables universal. The driver uses an explicit changed marker to repeat
1695
+ only productive rounds. `stable(Level)` raises the requested closure level and
1696
+ succeeds once that level has been reached. `becomes(From, To)` performs linear
1697
+ state replacement and prepares existing user predicates for mutation, so an
1698
+ EyeProlog source does not need a separate `dynamic/1` declaration merely to use
1699
+ that state with `becomes/2`.
1700
+
1701
+ The `:+` reasoning algorithm is therefore Prolog code in `src/lib/eyelet.pl`, not
1702
+ a second JavaScript implementation. The remaining JavaScript references have
1703
+ non-semantic roles: the parser declares the normal-profile operator, program
1704
+ analysis scans forward-rule premises for dependency/autoload planning, execution
1705
+ bootstraps the private `eyelet:eyelet_run/3` entry point, and two private library
1706
+ adapters bridge database mutability and answer/fuse events to the host. Those
1707
+ adapters are not part of the strict ISO registry or the public `library(eyelet)`
1708
+ export surface.
1694
1709
 
1695
1710
  The JavaScript convenience `run()` function selects this forward mode when no
1696
1711
  explicit `goal` or `goals` option is supplied. Advanced embedders can inspect a
@@ -6368,7 +6383,7 @@ bindings make one aligned subterm pair sufficient. For example:
6368
6383
  | `library(debug)` | `*/1`, `$/1`, `$-/1`, `debug/1`, `debug/3`, `nodebug/1`, `bb_get/2`, `bb_put/2`, `bb_b_put/2`, `bb_global_get/2` | Declarative debug operators and constraint-library blackboards |
6369
6384
  | `library(dif)` | `dif/2` | Common module facade over native delayed disequality |
6370
6385
  | `library(error)` | `must_be/2`, `can_be/2`, `instantiation_error/0`, `instantiation_error/1`, `domain_error/2`, `domain_error/3`, `type_error/2`, `type_error/3`, `representation_error/1`, `resource_error/1`, `call_with_error_context/2` | Error checking and construction |
6371
- | `library(eyelet)` | `stable/1`, `becomes/2` | Eyelet forward-reasoning state helpers; the `:+` operator is exported by the module and executed natively by EyeProlog |
6386
+ | `library(eyelet)` | `stable/1`, `becomes/2` | Eyelet forward-reasoning driver and state helpers; the `:+` operator is exported by the module and its fixed point is implemented in Prolog |
6372
6387
  | `library(format)` | `format_/4`, `format/2`, `format/3`, `listing/1`, `portray_clause_/3`, `portray_clause/1`, `portray_clause/2` | Formatted DCG text and output; `format_/4` and `portray_clause_/3` are the expanded nonterminals |
6373
6388
  | `library(freeze)` | `freeze/2` | Delayed goals |
6374
6389
  | `library(gensym)` | `gensym/2`, `reset_gensym/1` | Process-local generated atoms |
package/why-eyeprolog.md CHANGED
@@ -143,12 +143,15 @@ This avoids a heuristic silently changing answer multiplicity, termination, or
143
143
  resource behavior for an otherwise ordinary recursive predicate.
144
144
 
145
145
  Forward chaining is explicit too. Normal mode recognizes `Conclusion :+ Premise`
146
- and executes those rules with a native fixed-point driver. The bundled
147
- `library(eyelet)` exports the `:+` operator, `stable/1`, and `becomes/2`, including
148
- Eyelet's `true :+ Goal` query and `false :+ Goal` fuse conventions. Strict ISO mode
149
- removes the operator. Because resource exhaustion is not logical failure, normal
150
- execution has no hidden depth cutoff; an explicitly requested depth limit raises
151
- `resource_error(depth_limit)`.
146
+ and executes those rules with the Prolog fixed-point driver in the bundled
147
+ `library(eyelet)`. That module owns the Eyelet semantics—rule selection,
148
+ fixed-point iteration, skolemization, `stable/1`, `becomes/2`, and the
149
+ `true :+ Goal` / `false :+ Goal` control conventions—rather than duplicating the
150
+ reasoning algorithm in JavaScript. The remaining JavaScript support is limited to
151
+ syntax recognition, static dependency/autoload planning, driver bootstrap, and
152
+ private database-mutability/output-event adapters. Strict ISO mode removes the operator.
153
+ Because resource exhaustion is not logical failure, normal execution has no hidden
154
+ depth cutoff; an explicitly requested depth limit raises `resource_error(depth_limit)`.
152
155
 
153
156
  Library convenience is explicit at the profile boundary too. Outside strict ISO
154
157
  mode, an unresolved unqualified predicate may autoload its unique provider from