eyeprolog 1.5.4 → 1.5.6
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/datalog-common.js +56 -0
- package/src/datalog.js +10 -65
- package/src/execute.js +45 -276
- package/src/lib/eyelet.pl +271 -22
- package/src/parser.js +1 -1
- package/src/program.js +38 -1
- package/src/scryer-compat.js +71 -1
- package/src/wfs.js +11 -47
- package/test/run-regression.mjs +23 -5
- package/the-art-of-eyeprolog.md +24 -10
- package/why-eyeprolog.md +8 -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 only loads the driver and provides private mutability/output adapters. 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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Shared plumbing for the finite positive-Datalog and WFS evaluators.
|
|
2
|
+
//
|
|
3
|
+
// Keep evaluator-specific relation storage and fixpoint algorithms in their
|
|
4
|
+
// own modules; this file only contains representation/traversal helpers whose
|
|
5
|
+
// semantics are common to both execution paths.
|
|
6
|
+
|
|
7
|
+
import { ATOM, COMPOUND, VAR } from './term.js';
|
|
8
|
+
|
|
9
|
+
export const EMPTY_ARRAY = Object.freeze([]);
|
|
10
|
+
|
|
11
|
+
export function predicateKey(module, name, arity) {
|
|
12
|
+
return `${module ?? 'user'}:${name}/${arity}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function directLiteral(goal, module) {
|
|
16
|
+
if (goal?.type !== COMPOUND && goal?.type !== ATOM) return null;
|
|
17
|
+
return {
|
|
18
|
+
key: predicateKey(goal.module ?? module, goal.name, goal.arity),
|
|
19
|
+
name: goal.name,
|
|
20
|
+
arity: goal.arity,
|
|
21
|
+
module: goal.module ?? module,
|
|
22
|
+
args: goal.args ?? EMPTY_ARRAY,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function dependencyCone(program, rootGroup, literalForGoal = directLiteral) {
|
|
27
|
+
const groups = [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const stack = [rootGroup];
|
|
30
|
+
while (stack.length > 0) {
|
|
31
|
+
const group = stack.pop();
|
|
32
|
+
const key = predicateKey(group.module, group.name, group.arity);
|
|
33
|
+
if (seen.has(key)) continue;
|
|
34
|
+
seen.add(key);
|
|
35
|
+
groups.push(group);
|
|
36
|
+
for (const clause of group.clauses) {
|
|
37
|
+
for (const goal of clause.body) {
|
|
38
|
+
const literal = literalForGoal(goal, group.module);
|
|
39
|
+
if (!literal) continue;
|
|
40
|
+
const target = program.findGroup(literal.name, literal.arity, literal.module);
|
|
41
|
+
if (target) stack.push(target);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return groups;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolvePatternTerm(term, bindings) {
|
|
49
|
+
if (term.type === VAR) return bindings.get(term.name) ?? null;
|
|
50
|
+
return term;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function estimateLiteral(literal, relation, bindings) {
|
|
54
|
+
const candidates = relation.candidateIndexes(literal.args, bindings);
|
|
55
|
+
return candidates == null ? relation.rows.length : candidates.length;
|
|
56
|
+
}
|
package/src/datalog.js
CHANGED
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
// newly-added row fires only the rule occurrences that mention its predicate.
|
|
6
6
|
// This avoids replaying the whole recursive program on every table round.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { VAR } from './term.js';
|
|
9
9
|
import { numberValueKey } from './number-value.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
import {
|
|
11
|
+
EMPTY_ARRAY,
|
|
12
|
+
dependencyCone,
|
|
13
|
+
directLiteral,
|
|
14
|
+
estimateLiteral,
|
|
15
|
+
predicateKey,
|
|
16
|
+
resolvePatternTerm,
|
|
17
|
+
} from './datalog-common.js';
|
|
16
18
|
|
|
17
19
|
const scalarKeyCache = new WeakMap();
|
|
18
20
|
|
|
@@ -30,11 +32,6 @@ function sameScalar(left, right) {
|
|
|
30
32
|
return scalarKey(left) === scalarKey(right);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function tupleKey(tuple) {
|
|
34
|
-
if (tuple.length === 1) return scalarKey(tuple[0]);
|
|
35
|
-
return tuple.map(scalarKey).join('\u0001');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
35
|
export class DatalogRelation {
|
|
39
36
|
constructor(arity) {
|
|
40
37
|
this.arity = arity;
|
|
@@ -117,50 +114,16 @@ export class DatalogRelation {
|
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
function directLiteral(goal, module) {
|
|
121
|
-
if (goal?.type !== COMPOUND && goal?.type !== ATOM) return null;
|
|
122
|
-
return {
|
|
123
|
-
key: predicateKey(goal.module ?? module, goal.name, goal.arity),
|
|
124
|
-
name: goal.name,
|
|
125
|
-
arity: goal.arity,
|
|
126
|
-
module: goal.module ?? module,
|
|
127
|
-
args: goal.args ?? EMPTY_ARRAY,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function dependencyCone(program, rootGroup) {
|
|
132
|
-
const groups = [];
|
|
133
|
-
const seen = new Set();
|
|
134
|
-
const stack = [rootGroup];
|
|
135
|
-
while (stack.length > 0) {
|
|
136
|
-
const group = stack.pop();
|
|
137
|
-
const key = predicateKey(group.module, group.name, group.arity);
|
|
138
|
-
if (seen.has(key)) continue;
|
|
139
|
-
seen.add(key);
|
|
140
|
-
groups.push(group);
|
|
141
|
-
for (const clause of group.clauses) {
|
|
142
|
-
for (const goal of clause.body) {
|
|
143
|
-
const literal = directLiteral(goal, group.module);
|
|
144
|
-
if (!literal) continue;
|
|
145
|
-
const target = program.findGroup(literal.name, literal.arity, literal.module);
|
|
146
|
-
if (target) stack.push(target);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return groups;
|
|
151
|
-
}
|
|
152
117
|
|
|
153
118
|
function compileProgram(program, rootGroup) {
|
|
154
119
|
const groups = dependencyCone(program, rootGroup);
|
|
155
120
|
const relations = new Map();
|
|
156
|
-
const groupByKey = new Map();
|
|
157
121
|
const rules = [];
|
|
158
122
|
const triggers = new Map();
|
|
159
123
|
|
|
160
124
|
for (const group of groups) {
|
|
161
125
|
const key = predicateKey(group.module, group.name, group.arity);
|
|
162
126
|
relations.set(key, new DatalogRelation(group.arity));
|
|
163
|
-
groupByKey.set(key, group);
|
|
164
127
|
}
|
|
165
128
|
|
|
166
129
|
for (const group of groups) {
|
|
@@ -181,13 +144,9 @@ function compileProgram(program, rootGroup) {
|
|
|
181
144
|
}
|
|
182
145
|
}
|
|
183
146
|
|
|
184
|
-
return { groups,
|
|
147
|
+
return { groups, relations, rules, triggers };
|
|
185
148
|
}
|
|
186
149
|
|
|
187
|
-
function resolvePatternTerm(term, bindings) {
|
|
188
|
-
if (term.type === VAR) return bindings.get(term.name) ?? null;
|
|
189
|
-
return term;
|
|
190
|
-
}
|
|
191
150
|
|
|
192
151
|
function matchTupleMutable(args, tuple, bindings) {
|
|
193
152
|
const added = [];
|
|
@@ -218,10 +177,6 @@ function undoBindings(bindings, added) {
|
|
|
218
177
|
for (let i = added.length - 1; i >= 0; i--) bindings.delete(added[i]);
|
|
219
178
|
}
|
|
220
179
|
|
|
221
|
-
function estimateLiteral(literal, relation, bindings) {
|
|
222
|
-
const candidates = relation.candidateIndexes(literal.args, bindings);
|
|
223
|
-
return candidates == null ? relation.rows.length : candidates.length;
|
|
224
|
-
}
|
|
225
180
|
|
|
226
181
|
function forEachBinding(body, relations, callback, bindings, remaining) {
|
|
227
182
|
if (remaining.length === 0) {
|
|
@@ -271,16 +226,6 @@ function forEachBinding(body, relations, callback, bindings, remaining) {
|
|
|
271
226
|
}
|
|
272
227
|
}
|
|
273
228
|
|
|
274
|
-
function instantiateTuple(args, bindings) {
|
|
275
|
-
const tuple = new Array(args.length);
|
|
276
|
-
for (let i = 0; i < args.length; i++) {
|
|
277
|
-
const arg = args[i];
|
|
278
|
-
const value = arg.type === VAR ? bindings.get(arg.name) : arg;
|
|
279
|
-
if (value == null || value.type === VAR) return null;
|
|
280
|
-
tuple[i] = value;
|
|
281
|
-
}
|
|
282
|
-
return tuple;
|
|
283
|
-
}
|
|
284
229
|
|
|
285
230
|
export function evaluatePositiveDatalog(program, rootGroup) {
|
|
286
231
|
const compiled = compileProgram(program, rootGroup);
|
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
|
@@ -159,7 +159,7 @@ export const ISO_OPERATOR_DEFINITIONS = [
|
|
|
159
159
|
export const PART3_OPERATOR_DEFINITIONS = [
|
|
160
160
|
[1105, 'xfy', '|'],
|
|
161
161
|
// EyeProlog forward-rule extension. A top-level Conclusion :+ Premise is
|
|
162
|
-
// executed by
|
|
162
|
+
// executed by library(eyelet)'s Prolog closure driver when no explicit CLI goal is given.
|
|
163
163
|
[1200, 'xfx', ':+'],
|
|
164
164
|
// Tabling is explicit in the normal EyeProlog profile. Make the common
|
|
165
165
|
// `:- table p/n.` declaration available without requiring a library import.
|
package/src/program.js
CHANGED
|
@@ -1030,6 +1030,43 @@ function hybridFastSource(source, options) {
|
|
|
1030
1030
|
// that phase, so extend the existing instance with the same canonical interop
|
|
1031
1031
|
// imports before solving those goals. Keeping this here lets both paths share
|
|
1032
1032
|
// the dependency analysis and module-loading rules.
|
|
1033
|
+
// Load one bundled library module into an already-built Program without
|
|
1034
|
+
// importing its public predicates into user. This is useful for host bootstraps
|
|
1035
|
+
// that call a module-private Prolog entry point while keeping the user-facing
|
|
1036
|
+
// module export list unchanged.
|
|
1037
|
+
export function loadBundledProgramLibrary(program, name, options = {}) {
|
|
1038
|
+
if (program.modules.has(name)) return program;
|
|
1039
|
+
const operatorState = createParserOperatorState(
|
|
1040
|
+
[...program.operators.values()],
|
|
1041
|
+
false,
|
|
1042
|
+
{ isoStrict: program.strictIso },
|
|
1043
|
+
);
|
|
1044
|
+
const parserFlagState = {
|
|
1045
|
+
doubleQuotes: program.doubleQuotes ?? options.doubleQuotes ?? 'chars',
|
|
1046
|
+
charConversion: 'on',
|
|
1047
|
+
charConversions: new Map(),
|
|
1048
|
+
};
|
|
1049
|
+
const builder = new ProgramBuilder({ ...options, isoStrict: program.strictIso }, program);
|
|
1050
|
+
const loadedModules = new Set([...program.modules.keys()].filter((module) => module !== 'user'));
|
|
1051
|
+
const ensured = new Set();
|
|
1052
|
+
const designation = compound('library', [atom(name)]);
|
|
1053
|
+
const loaded = readModuleSource(designation, {
|
|
1054
|
+
...options,
|
|
1055
|
+
isoStrict: program.strictIso,
|
|
1056
|
+
operatorState,
|
|
1057
|
+
parserFlagState,
|
|
1058
|
+
});
|
|
1059
|
+
const childContext = { module: loaded.name };
|
|
1060
|
+
if (!loadSourceIntoBuilder(
|
|
1061
|
+
builder, loaded.text, loaded.options, ensured, loadedModules, false, childContext,
|
|
1062
|
+
)) {
|
|
1063
|
+
throw new Error(`could not load library(${name})`);
|
|
1064
|
+
}
|
|
1065
|
+
program.noteMutation(false);
|
|
1066
|
+
builder.finish();
|
|
1067
|
+
return program;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1033
1070
|
export function autoloadProgramGoals(program, inputs, options = {}) {
|
|
1034
1071
|
if (inputs == null || (Array.isArray(inputs) && inputs.length === 0)) return program;
|
|
1035
1072
|
const operatorState = createParserOperatorState(
|
|
@@ -1131,7 +1168,7 @@ function bundledLibraryModule(program, module) {
|
|
|
1131
1168
|
function groupDependencies(group) {
|
|
1132
1169
|
const dependencies = [];
|
|
1133
1170
|
for (const clause of group.clauses) {
|
|
1134
|
-
//
|
|
1171
|
+
// Eyelet forward rules store their executable premise in the :+/2 head
|
|
1135
1172
|
// rather than in the ordinary clause body. Treat that premise as a goal
|
|
1136
1173
|
// dependency so bundled predicates such as stable/1 and becomes/2 can be
|
|
1137
1174
|
// autoloaded exactly like calls in ordinary rules. Derived :+ rules may
|
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/src/wfs.js
CHANGED
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import { ATOM, COMPOUND, VAR } from './term.js';
|
|
14
14
|
import { numberValueKey } from './number-value.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
import {
|
|
16
|
+
EMPTY_ARRAY,
|
|
17
|
+
dependencyCone,
|
|
18
|
+
directLiteral,
|
|
19
|
+
estimateLiteral,
|
|
20
|
+
predicateKey,
|
|
21
|
+
resolvePatternTerm,
|
|
22
|
+
} from './datalog-common.js';
|
|
19
23
|
|
|
20
24
|
function scalarKey(term) {
|
|
21
25
|
if (term.type === 'number') return `number\u0000${numberValueKey(term.name)}`;
|
|
@@ -73,8 +77,6 @@ class Relation {
|
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
const EMPTY_ARRAY = Object.freeze([]);
|
|
77
|
-
|
|
78
80
|
function emptyRelations(groups) {
|
|
79
81
|
const relations = new Map();
|
|
80
82
|
for (const group of groups) {
|
|
@@ -93,48 +95,18 @@ function copyBaseRelations(base, groups) {
|
|
|
93
95
|
return relations;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
function directLiteral(goal, module) {
|
|
97
|
-
if (goal?.type !== COMPOUND && goal?.type !== ATOM) return null;
|
|
98
|
-
return {
|
|
99
|
-
key: predicateKey(goal.module ?? module, goal.name, goal.arity),
|
|
100
|
-
name: goal.name,
|
|
101
|
-
arity: goal.arity,
|
|
102
|
-
module: goal.module ?? module,
|
|
103
|
-
args: goal.args ?? EMPTY_ARRAY,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
98
|
|
|
107
99
|
function negativeLiteral(goal, module) {
|
|
108
100
|
if (goal?.type !== COMPOUND || goal.name !== 'tnot' || goal.arity !== 1) return null;
|
|
109
101
|
return directLiteral(goal.args[0], module);
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
const seen = new Set();
|
|
115
|
-
const stack = [rootGroup];
|
|
116
|
-
while (stack.length > 0) {
|
|
117
|
-
const group = stack.pop();
|
|
118
|
-
const key = predicateKey(group.module, group.name, group.arity);
|
|
119
|
-
if (seen.has(key)) continue;
|
|
120
|
-
seen.add(key);
|
|
121
|
-
groups.push(group);
|
|
122
|
-
|
|
123
|
-
for (const clause of group.clauses) {
|
|
124
|
-
for (const goal of clause.body) {
|
|
125
|
-
const neg = negativeLiteral(goal, group.module);
|
|
126
|
-
const literal = neg ?? directLiteral(goal, group.module);
|
|
127
|
-
if (!literal) continue;
|
|
128
|
-
const target = program.findGroup(literal.name, literal.arity, literal.module);
|
|
129
|
-
if (target) stack.push(target);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return groups;
|
|
104
|
+
function dependencyLiteral(goal, module) {
|
|
105
|
+
return negativeLiteral(goal, module) ?? directLiteral(goal, module);
|
|
134
106
|
}
|
|
135
107
|
|
|
136
108
|
function compileProgram(program, rootGroup) {
|
|
137
|
-
const groups = dependencyCone(program, rootGroup);
|
|
109
|
+
const groups = dependencyCone(program, rootGroup, dependencyLiteral);
|
|
138
110
|
const base = emptyRelations(groups);
|
|
139
111
|
const rules = [];
|
|
140
112
|
|
|
@@ -175,10 +147,6 @@ function compileProgram(program, rootGroup) {
|
|
|
175
147
|
return { groups, base, rules };
|
|
176
148
|
}
|
|
177
149
|
|
|
178
|
-
function resolvePatternTerm(term, bindings) {
|
|
179
|
-
if (term.type === VAR) return bindings.get(term.name) ?? null;
|
|
180
|
-
return term;
|
|
181
|
-
}
|
|
182
150
|
|
|
183
151
|
function matchTuple(args, tuple, bindings) {
|
|
184
152
|
let next = null;
|
|
@@ -199,10 +167,6 @@ function matchTuple(args, tuple, bindings) {
|
|
|
199
167
|
return next ?? bindings;
|
|
200
168
|
}
|
|
201
169
|
|
|
202
|
-
function estimateLiteral(literal, relation, bindings) {
|
|
203
|
-
const candidates = relation.candidateIndexes(literal.args, bindings);
|
|
204
|
-
return candidates == null ? relation.rows.length : candidates.length;
|
|
205
|
-
}
|
|
206
170
|
|
|
207
171
|
function forEachPositiveBinding(positives, relations, callback, bindings = new Map(), remaining = null) {
|
|
208
172
|
if (remaining == null) remaining = positives.map((_, index) => index);
|
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,33 @@ 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 host boundary is intentionally small:
|
|
1703
|
+
JavaScript bootstraps the private `eyelet:eyelet_run/3` entry point and supplies
|
|
1704
|
+
two private library adapters—one to make an existing user procedure mutable
|
|
1705
|
+
without rewriting its clauses, and one to forward answer/fuse terms to the CLI
|
|
1706
|
+
or embedding callbacks. Those adapters are not part of the strict ISO registry
|
|
1707
|
+
or the public `library(eyelet)` export surface.
|
|
1694
1708
|
|
|
1695
1709
|
The JavaScript convenience `run()` function selects this forward mode when no
|
|
1696
1710
|
explicit `goal` or `goals` option is supplied. Advanced embedders can inspect a
|
|
@@ -6368,7 +6382,7 @@ bindings make one aligned subterm pair sufficient. For example:
|
|
|
6368
6382
|
| `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
6383
|
| `library(dif)` | `dif/2` | Common module facade over native delayed disequality |
|
|
6370
6384
|
| `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
|
|
6385
|
+
| `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
6386
|
| `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
6387
|
| `library(freeze)` | `freeze/2` | Delayed goals |
|
|
6374
6388
|
| `library(gensym)` | `gensym/2`, `reset_gensym/1` | Process-local generated atoms |
|
package/why-eyeprolog.md
CHANGED
|
@@ -143,12 +143,14 @@ 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 host layer is limited to private database
|
|
151
|
+
mutability and output-event adapters. Strict ISO mode removes the operator.
|
|
152
|
+
Because resource exhaustion is not logical failure, normal execution has no hidden
|
|
153
|
+
depth cutoff; an explicitly requested depth limit raises `resource_error(depth_limit)`.
|
|
152
154
|
|
|
153
155
|
Library convenience is explicit at the profile boundary too. Outside strict ISO
|
|
154
156
|
mode, an unresolved unqualified predicate may autoload its unique provider from
|