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 +4 -8
- package/package.json +1 -1
- package/src/execute.js +45 -276
- package/src/lib/eyelet.pl +271 -22
- package/src/parser.js +5 -13
- package/src/program.js +56 -131
- package/src/scryer-compat.js +71 -1
- package/test/run-regression.mjs +23 -5
- package/the-art-of-eyeprolog.md +25 -10
- package/why-eyeprolog.md +9 -6
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
|
-
##
|
|
60
|
-
Normal mode executes top-level `Conclusion :+ Premise` rules
|
|
61
|
-
|
|
62
|
-
`
|
|
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
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,
|
|
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 {
|
|
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
|
-
//
|
|
60
|
+
// Eyelet forward-rule bootstrap.
|
|
61
61
|
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
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
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
194
|
-
return
|
|
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.
|
|
205
|
-
//
|
|
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
|
|
1
|
+
% Eyelet forward reasoning for EyeProlog.
|
|
2
2
|
%
|
|
3
|
-
% The
|
|
4
|
-
%
|
|
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, [
|
|
6
|
+
:- module(eyelet, [
|
|
7
|
+
op(1200, xfx, :+),
|
|
8
|
+
stable/1,
|
|
9
|
+
becomes/2
|
|
10
|
+
]).
|
|
8
11
|
|
|
9
|
-
:- use_module(library(iso_ext), [
|
|
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
|
|
16
|
-
% level
|
|
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
|
-
|
|
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
|
-
|
|
215
|
+
user:closure(Closure),
|
|
25
216
|
Level =< Closure.
|
|
26
217
|
|
|
27
218
|
% becomes(:From, :To)
|
|
28
|
-
% Linear implication over dynamic state
|
|
29
|
-
%
|
|
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
|
-
|
|
222
|
+
dynify_goal(From),
|
|
223
|
+
dynify_goal(To),
|
|
224
|
+
catch(user:From, _, fail),
|
|
32
225
|
conj_list(From, Old),
|
|
33
|
-
|
|
226
|
+
retract_conj(Old),
|
|
34
227
|
conj_list(To, New),
|
|
35
|
-
|
|
228
|
+
assert_conj_list(New).
|
|
36
229
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
1135
|
-
//
|
|
1136
|
-
//
|
|
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
|
-
|
|
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
|
|
1097
|
+
function collectForwardRulePremiseDependencies(term, collect, out) {
|
|
1161
1098
|
if (term?.type !== COMPOUND) return out;
|
|
1162
1099
|
if (term.name === ':+' && term.arity === 2) {
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
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 === ':+')
|
|
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({
|
package/src/scryer-compat.js
CHANGED
|
@@ -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 });
|
package/test/run-regression.mjs
CHANGED
|
@@ -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 `:+`', '
|
|
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
|
|
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: '
|
|
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,
|
|
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');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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,
|
|
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
|
-
###
|
|
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
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
|
1693
|
-
`stable
|
|
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
|
|
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
|
|
147
|
-
`library(eyelet)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|