eyeprolog 1.5.3 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,10 +57,10 @@ printf 'human(socrates).\nmortal(X) :- human(X).\n' |
57
57
  ```
58
58
 
59
59
  ## Native forward rules (`:+`)
60
-
61
60
  Normal mode executes top-level `Conclusion :+ Premise` rules natively when no explicit
62
61
  `-g/--goal` is supplied. `true :+ Goal` prints answers and `false :+ Goal` emits a
63
- `fuse/1`; JavaScript `run()` follows the same rule. Strict ISO mode disables this
62
+ `fuse/1`; JavaScript `run()` follows the same rule. The bundled `library(eyelet)`
63
+ exports the `:+` operator, `stable/1`, and `becomes/2`. Strict ISO mode disables this
64
64
  extension. Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL;
65
65
  use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce
66
66
  operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.3",
6
+ "version": "1.5.5",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -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 { ATOM, COMPOUND, VAR } from './term.js';
8
+ import { VAR } from './term.js';
9
9
  import { numberValueKey } from './number-value.js';
10
-
11
- const EMPTY_ARRAY = Object.freeze([]);
12
-
13
- function predicateKey(module, name, arity) {
14
- return `${module ?? 'user'}:${name}/${arity}`;
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, groupByKey, relations, rules, triggers };
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
@@ -82,8 +82,6 @@ export function executeForwardRules(program, solver, {
82
82
  predeclareForwardTerms(program, solver, group);
83
83
  setSingletonFact(program, 'closure', [numberTerm(0)]);
84
84
  setSingletonFact(program, 'limit', [numberTerm(-1)]);
85
- setSingletonFact(program, 'count', [atom('fm'), numberTerm(0)], true);
86
- setSingletonFact(program, 'count', [atom('mf'), numberTerm(0)], true);
87
85
 
88
86
  const seenAnswers = new Set();
89
87
  let rounds = 0;
@@ -188,11 +186,6 @@ export function executeForwardRules(program, solver, {
188
186
  break;
189
187
  }
190
188
 
191
- for (const name of ['fm', 'mf']) {
192
- const count = keyedCount(program, name);
193
- if (count !== 0) onDiagnostic(`*** ${name}=${count}\n`);
194
- }
195
-
196
189
  return { haltCode, rounds, derived };
197
190
  }
198
191
 
@@ -370,12 +363,4 @@ function singletonInteger(program, name, fallback) {
370
363
  return fallback;
371
364
  }
372
365
 
373
- function keyedCount(program, key) {
374
- const group = program.findGroup('count', 2, 'user');
375
- if (group == null) return 0;
376
- for (let i = group.clauses.length - 1; i >= 0; i--) {
377
- const args = group.clauses[i].head?.args;
378
- if (args?.[0]?.type === ATOM && args[0].name === key && args[1]?.type === NUMBER) return Number(args[1].name);
379
- }
380
- return 0;
381
- }
366
+
@@ -0,0 +1,43 @@
1
+ % Eyelet forward-reasoning support for EyeProlog.
2
+ %
3
+ % The :+/2 closure itself is executed natively by EyeProlog. This module owns
4
+ % the two state helpers used by Eyelet programs and exports the :+ operator so
5
+ % source files can opt into the complete surface with one use_module/1.
6
+
7
+ :- module(eyelet, [op(1200, xfx, :+), stable/1, becomes/2]).
8
+
9
+ :- use_module(library(iso_ext), [forall/2]).
10
+ :- use_module(library(lists), [member/2]).
11
+
12
+ :- meta_predicate(becomes(0, 0)).
13
+
14
+ % stable(+Level)
15
+ % Fail until the native forward-rule driver has reached the requested closure
16
+ % level. Asking for a higher level extends the driver's target limit.
17
+ stable(Level) :-
18
+ call(user:limit(Limit)),
19
+ ( Limit < Level
20
+ -> user:retract(limit(Limit)),
21
+ user:assertz(limit(Level))
22
+ ; true
23
+ ),
24
+ call(user:closure(Closure)),
25
+ Level =< Closure.
26
+
27
+ % becomes(:From, :To)
28
+ % Linear implication over dynamic state: prove and retract every conjunct in
29
+ % From, then assert every conjunct in To.
30
+ becomes(From, To) :-
31
+ catch(call(user:From), _, fail),
32
+ conj_list(From, Old),
33
+ forall(member(Clause, Old), user:retract(Clause)),
34
+ conj_list(To, New),
35
+ forall(member(Clause, New), user:assertz(Clause)).
36
+
37
+ conj_list(true, []).
38
+ conj_list(Goal, [Goal]) :-
39
+ Goal \= (_, _),
40
+ Goal \= false,
41
+ !.
42
+ conj_list((A, B), [A|Rest]) :-
43
+ conj_list(B, Rest).
@@ -69,6 +69,7 @@ export const eyePrologLibraryAutoload = Object.freeze({
69
69
  "bb_get/2": "debug",
70
70
  "bb_global_get/2": "debug",
71
71
  "bb_put/2": "debug",
72
+ "becomes/2": "eyelet",
72
73
  "between/3": "between",
73
74
  "call_nth/2": "iso_ext",
74
75
  "call_residue_vars/2": "atts",
@@ -264,6 +265,7 @@ export const eyePrologLibraryAutoload = Object.freeze({
264
265
  "slice/4": "lists",
265
266
  "smallest_divisor_from/3": "primes",
266
267
  "split/3": "strings",
268
+ "stable/1": "eyelet",
267
269
  "start_tabling/2": "tabling",
268
270
  "string_concat/3": "strings",
269
271
  "substring/4": "strings",
@@ -322,6 +324,7 @@ export const eyePrologLibraryAutoloadModules = Object.freeze([
322
324
  "debug",
323
325
  "dif",
324
326
  "error",
327
+ "eyelet",
325
328
  "format",
326
329
  "freeze",
327
330
  "gensym",
package/src/program.js CHANGED
@@ -1131,6 +1131,14 @@ function bundledLibraryModule(program, module) {
1131
1131
  function groupDependencies(group) {
1132
1132
  const dependencies = [];
1133
1133
  for (const clause of group.clauses) {
1134
+ // Native forward rules store their executable premise in the :+/2 head
1135
+ // rather than in the ordinary clause body. Treat that premise as a goal
1136
+ // dependency so bundled predicates such as stable/1 and becomes/2 can be
1137
+ // autoloaded exactly like calls in ordinary rules. Derived :+ rules may
1138
+ // themselves occur in a conclusion, so recurse through that shape too.
1139
+ if (group.name === ':+') {
1140
+ dependencies.push(...forwardRuleDependencies(clause.head));
1141
+ }
1134
1142
  if (isCompactBinaryClause(clause)) {
1135
1143
  if (clause.bodyName != null) {
1136
1144
  dependencies.push({
@@ -1149,6 +1157,103 @@ function groupDependencies(group) {
1149
1157
  return dependencies;
1150
1158
  }
1151
1159
 
1160
+ function forwardRuleDependencies(term, out = []) {
1161
+ if (term?.type !== COMPOUND) return out;
1162
+ if (term.name === ':+' && term.arity === 2) {
1163
+ out.push(...collectGoalDependencies(term.args[1], false, true));
1164
+ forwardRuleDependencies(term.args[0], out);
1165
+ return out;
1166
+ }
1167
+ if (term.name === ',' && term.arity === 2) {
1168
+ forwardRuleDependencies(term.args[0], out);
1169
+ forwardRuleDependencies(term.args[1], out);
1170
+ }
1171
+ return out;
1172
+ }
1173
+
1174
+ // Autoloading needs a slightly richer dependency walk than recursion analysis.
1175
+ // Meta-predicates such as forall/2 and setof/3 are themselves callable
1176
+ // procedures while also containing executable goals. The recursion graph
1177
+ // intentionally looks through those wrappers; the autoloader must see both the
1178
+ // wrapper and any statically visible nested calls so a bundled library can be
1179
+ // loaded before execution reaches it.
1180
+ function collectAutoloadGoalDependencies(goal, out = []) {
1181
+ if (goal?.type === ATOM) {
1182
+ out.push({ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module });
1183
+ return out;
1184
+ }
1185
+ if (goal?.type !== COMPOUND) return out;
1186
+ if (goal.name === ',' && goal.arity === 2) {
1187
+ collectAutoloadGoalDependencies(goal.args[0], out);
1188
+ collectAutoloadGoalDependencies(goal.args[1], out);
1189
+ return out;
1190
+ }
1191
+ if ((goal.name === ';' || goal.name === '->') && goal.arity === 2) {
1192
+ collectAutoloadGoalDependencies(goal.args[0], out);
1193
+ collectAutoloadGoalDependencies(goal.args[1], out);
1194
+ return out;
1195
+ }
1196
+ if ((goal.name === '\\+' || goal.name === 'not' || goal.name === 'tnot' || goal.name === 'once') && goal.arity === 1) {
1197
+ collectAutoloadGoalDependencies(goal.args[0], out);
1198
+ return out;
1199
+ }
1200
+
1201
+ out.push({ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module });
1202
+
1203
+ if (goal.name === 'forall' && goal.arity === 2) {
1204
+ collectAutoloadGoalDependencies(goal.args[0], out);
1205
+ collectAutoloadGoalDependencies(goal.args[1], out);
1206
+ } else if ((goal.name === 'findall' || goal.name === 'bagof' || goal.name === 'setof' || goal.name === 'sumall') && goal.arity === 3) {
1207
+ collectAutoloadGoalDependencies(goal.args[1], out);
1208
+ } else if (goal.name === 'countall' && goal.arity === 2) {
1209
+ collectAutoloadGoalDependencies(goal.args[0], out);
1210
+ } else if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
1211
+ collectAutoloadGoalDependencies(goal.args[2], out);
1212
+ } else if (goal.name === 'catch' && goal.arity === 3) {
1213
+ collectAutoloadGoalDependencies(goal.args[0], out);
1214
+ collectAutoloadGoalDependencies(goal.args[2], out);
1215
+ } else if ((goal.name === 'call_cleanup' || goal.name === 'setup_call_cleanup') && (goal.arity === 2 || goal.arity === 3)) {
1216
+ for (const arg of goal.args) collectAutoloadGoalDependencies(arg, out);
1217
+ } else if (goal.name === 'call' && goal.arity === 1) {
1218
+ collectAutoloadGoalDependencies(goal.args[0], out);
1219
+ }
1220
+ return out;
1221
+ }
1222
+
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
+ function groupAutoloadDependencies(group) {
1238
+ const dependencies = [];
1239
+ for (const clause of group.clauses) {
1240
+ if (group.name === ':+') dependencies.push(...forwardRuleAutoloadDependencies(clause.head));
1241
+ if (isCompactBinaryClause(clause)) {
1242
+ if (clause.bodyName != null) {
1243
+ dependencies.push({
1244
+ key: `${clause.bodyName}/2`,
1245
+ name: clause.bodyName,
1246
+ arity: 2,
1247
+ module: clause.module ?? group.module,
1248
+ });
1249
+ }
1250
+ continue;
1251
+ }
1252
+ for (const goal of clause.body) collectAutoloadGoalDependencies(goal, dependencies);
1253
+ }
1254
+ return dependencies;
1255
+ }
1256
+
1152
1257
  function parseInteropGoalInputs(inputs, options, program) {
1153
1258
  if (inputs == null) return [];
1154
1259
  const values = Array.isArray(inputs) ? inputs : [inputs];
@@ -1164,7 +1269,7 @@ function parseInteropGoalInputs(inputs, options, program) {
1164
1269
 
1165
1270
  function extraGoalDependencies(goals) {
1166
1271
  const dependencies = [];
1167
- for (const goal of goals) dependencies.push(...collectGoalDependencies(goal, false, true));
1272
+ for (const goal of goals) collectAutoloadGoalDependencies(goal, dependencies);
1168
1273
  return dependencies;
1169
1274
  }
1170
1275
 
@@ -1190,7 +1295,7 @@ function libraryAutoloadRequests(program, extraGoals = []) {
1190
1295
  const requests = new Map();
1191
1296
  for (const group of program.groups.values()) {
1192
1297
  if (bundledLibraryModule(program, group.module)) continue;
1193
- for (const dependency of groupDependencies(group)) {
1298
+ for (const dependency of groupAutoloadDependencies(group)) {
1194
1299
  const targetModule = dependency.module ?? group.module;
1195
1300
  if (procedureResolvedBeforeAutoload(program, dependency, targetModule)) continue;
1196
1301
  const library = autoloadLibraryFor(dependency);
@@ -1206,7 +1311,7 @@ function libraryAutoloadRequests(program, extraGoals = []) {
1206
1311
  }
1207
1312
  }
1208
1313
  for (const goal of program.initializations) {
1209
- for (const dependency of collectGoalDependencies(goal, false, true)) {
1314
+ for (const dependency of collectAutoloadGoalDependencies(goal)) {
1210
1315
  const targetModule = dependency.module ?? 'user';
1211
1316
  if (procedureResolvedBeforeAutoload(program, dependency, targetModule)) continue;
1212
1317
  const library = autoloadLibraryFor(dependency);
@@ -28,6 +28,7 @@ const moduleFiles = Object.freeze({
28
28
  debug: 'debug.pl',
29
29
  dif: 'dif.pl',
30
30
  error: 'error.pl',
31
+ eyelet: 'eyelet.pl',
31
32
  format: 'format.pl',
32
33
  freeze: 'freeze.pl',
33
34
  gensym: 'gensym.pl',
@@ -102,6 +103,7 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
102
103
  'sat/1', 'taut/2', 'labeling/1', 'sat_count/2', 'random_labeling/2', 'weighted_maximum/3',
103
104
  'lt/2', 'gt/2', 'le/2', 'ge/2',
104
105
  'difference/3',
106
+ 'stable/1', 'becomes/2',
105
107
  'seq/3', 'seqq/3',
106
108
  'debug/1', 'debug/3', 'nodebug/1', 'bb_get/2', 'bb_put/2', 'bb_b_put/2', 'bb_global_get/2',
107
109
  '*/1', '$/1', '$-/1',
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
- function predicateKey(module, name, arity) {
17
- return `${module ?? 'user'}:${name}/${arity}`;
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 dependencyCone(program, rootGroup) {
113
- const groups = [];
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);
@@ -4807,8 +4807,10 @@ ${profile}`;
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
4809
  assertIncludes(book, 'EyeProlog normal mode also accepts `:+`', 'native forward-rule extension');
4810
+ assertIncludes(book, '`library(eyelet)`', 'Eyelet library surface');
4811
+ assertIncludes(readme, '`library(eyelet)`', 'README Eyelet library surface');
4810
4812
  assertIncludes(profile, 'an unresolved unqualified predicate may autoload its unique provider', 'Why EyeProlog autoload policy');
4811
- assertIncludes(readme, 'Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL', 'README autoload summary');
4813
+ assertIncludes(readme, 'Bundled `src/lib/` predicates autoload in files, CLI/API goals,', 'README autoload summary');
4812
4814
  },
4813
4815
  },
4814
4816
  {
@@ -5469,6 +5471,20 @@ function apiCases() {
5469
5471
  assertEqual(result.haltCode, null, 'forward halt code');
5470
5472
  },
5471
5473
  },
5474
+ {
5475
+ name: 'native forward rules autoload library(eyelet) helpers from :+ premises',
5476
+ run: () => {
5477
+ const result = run(`
5478
+ :- dynamic(state/1).
5479
+ state(a).
5480
+ changed :+ becomes(state(a), state(b)).
5481
+ seed :+ state(b).
5482
+ ready :+ seed, stable(1).
5483
+ true :+ ready.
5484
+ `);
5485
+ assertEqual(result.stdout, 'ready.\n', 'forward helper stdout');
5486
+ },
5487
+ },
5472
5488
  {
5473
5489
  name: 'run exposes false/0 as an always-failing built-in',
5474
5490
  run: () => {
@@ -6287,7 +6303,7 @@ answer(ok) :-
6287
6303
  assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
6288
6304
  assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
6289
6305
  assertEqual(registeredNativeEyePrologLibraryNames().length, 58, 'public host-supported EyeProlog library count');
6290
- assertEqual(eyePrologPortableLibraryIndicators.length, 241, 'portable Prolog library count');
6306
+ assertEqual(eyePrologPortableLibraryIndicators.length, 243, 'portable Prolog library count');
6291
6307
  assertEqual(eyePrologInteropLibraryIndicators.length, 189, 'cross-implementation interop profile count');
6292
6308
  assertEqual(eyePrologInteropLibraryModules.length, 23, 'common explicit library module profile count');
6293
6309
  assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
@@ -6299,11 +6315,13 @@ answer(ok) :-
6299
6315
  assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'set_nth0/4 is outside the conservative interop subset');
6300
6316
  assertEqual(eyePrologLibraryAutoload['set_nth0/4'], 'lists', 'complete library autoload includes EyeProlog-only exports');
6301
6317
  assertEqual(eyePrologLibraryAutoload['pairs_keys_values/3'], 'pairs', 'complete library autoload includes library(pairs)');
6302
- assertEqual(eyePrologLibraryAutoloadModules.length, 35, 'all bundled src/lib modules are indexed for autoload');
6318
+ assertEqual(eyePrologLibraryAutoload['stable/1'], 'eyelet', 'complete library autoload includes Eyelet stable/1');
6319
+ assertEqual(eyePrologLibraryAutoload['becomes/2'], 'eyelet', 'complete library autoload includes Eyelet becomes/2');
6320
+ assertEqual(eyePrologLibraryAutoloadModules.length, 36, 'all bundled src/lib modules are indexed for autoload');
6303
6321
  assertEqual(eyePrologNativeLibraryIndicators.length, 58, 'host-supported library count');
6304
6322
  assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
6305
6323
  assertEqual(eyePrologNativeLibraryIndicators.includes('random/1'), true, 'stateful random/1 is classified as host-supported');
6306
- assertEqual(eyePrologLibraryIndicators.length, 299, 'complete non-ISO EyeProlog library surface');
6324
+ assertEqual(eyePrologLibraryIndicators.length, 301, 'complete non-ISO EyeProlog library surface');
6307
6325
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
6308
6326
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
6309
6327
  assertEqual(Boolean(library.get('eyeprolog__call_residue_vars', 2)), true, 'private call_residue_vars adapter is registered for EyeProlog');
@@ -1681,16 +1681,16 @@ is a forward rule. When a loaded program contains such rules and no explicit
1681
1681
  top-level goal overrides them, the engine runs a native closure driver rather
1682
1682
  than a Prolog meta-interpreter. It repeatedly solves premises against the
1683
1683
  current program, asserts novel conjuncts from successful conclusions, and
1684
- continues until no new conclusion is added. This is the execution path used by
1685
- the Eyelet adapter.
1684
+ continues until no new conclusion is added. This is the execution path used when
1685
+ Eyelet sources are run directly by EyeProlog.
1686
1686
 
1687
1687
  Two conclusions have control meaning. `true :+ Goal` is a query and prints each
1688
1688
  distinct successful instance of `Goal`. `false :+ Goal` is an integrity fuse:
1689
1689
  on success EyeProlog prints `fuse(Goal)` and returns halt status 2. Variables
1690
1690
  that occur only in an ordinary derived conclusion are existential and become
1691
1691
  `sk_0`, `sk_1`, and so on; a derived conclusion that is itself a `:+` rule keeps
1692
- its variables universal. Eyelet's `stable/1` and `becomes/2` remain library-level
1693
- compatibility helpers, while closure selection and assertion are native.
1692
+ its variables universal. The bundled `library(eyelet)` exports the `:+` operator together with
1693
+ `stable/1` and `becomes/2`, while closure selection and assertion are native.
1694
1694
 
1695
1695
  The JavaScript convenience `run()` function selects this forward mode when no
1696
1696
  explicit `goal` or `goals` option is supplied. Advanced embedders can inspect a
@@ -6278,12 +6278,12 @@ so side effects occur in Prolog execution order.
6278
6278
 
6279
6279
  ### The EyeProlog library
6280
6280
 
6281
- EyeProlog exposes **299 distinct non-ISO library and normal-extension predicate
6281
+ EyeProlog exposes **301 distinct non-ISO library and normal-extension predicate
6282
6282
  indicators** in addition to the 129 indicators in its isolated ISO profile.
6283
- **241 are defined entirely as ordinary Prolog clauses** in focused modules under
6283
+ **243 are defined entirely as ordinary Prolog clauses** in focused modules under
6284
6284
  `src/lib/`; **58 use host support** for control, attributed variables,
6285
6285
  constraints, or observability. The ISO and library catalogs therefore cover
6286
- **428 distinct predicate indicators**. Normal-mode controls and observability
6286
+ **430 distinct predicate indicators**. Normal-mode controls and observability
6287
6287
  relations that do not belong to a library, such as `call_cleanup/2`,
6288
6288
  `setup_call_cleanup/3`, `statistics/0,2`, and `tnot/1`, are documented in their
6289
6289
  own sections rather than counted as library predicates. `time/1` is additionally
@@ -6296,7 +6296,7 @@ The sources are `src/lib/aggregate.pl`, `src/lib/arithmetic.pl`,
6296
6296
  `src/lib/charsio.pl`, `src/lib/clpb.pl`, `src/lib/clpz.pl`,
6297
6297
  `src/lib/comparison.pl`, `src/lib/dates.pl`, `src/lib/dcgs.pl`,
6298
6298
  `src/lib/debug.pl`, `src/lib/dif.pl`, `src/lib/error.pl`,
6299
- `src/lib/format.pl`, `src/lib/freeze.pl`, `src/lib/gensym.pl`,
6299
+ `src/lib/eyelet.pl`, `src/lib/format.pl`, `src/lib/freeze.pl`, `src/lib/gensym.pl`,
6300
6300
  `src/lib/iso_ext.pl`, `src/lib/lambda.pl`, `src/lib/lists.pl`,
6301
6301
  `src/lib/ordsets.pl`, `src/lib/pairs.pl`, `src/lib/pio.pl`,
6302
6302
  `src/lib/primes.pl`, `src/lib/prologue.pl`, `src/lib/random.pl`,
@@ -6368,6 +6368,7 @@ bindings make one aligned subterm pair sufficient. For example:
6368
6368
  | `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
6369
  | `library(dif)` | `dif/2` | Common module facade over native delayed disequality |
6370
6370
  | `library(error)` | `must_be/2`, `can_be/2`, `instantiation_error/0`, `instantiation_error/1`, `domain_error/2`, `domain_error/3`, `type_error/2`, `type_error/3`, `representation_error/1`, `resource_error/1`, `call_with_error_context/2` | Error checking and construction |
6371
+ | `library(eyelet)` | `stable/1`, `becomes/2` | Eyelet forward-reasoning state helpers; the `:+` operator is exported by the module and executed natively by EyeProlog |
6371
6372
  | `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 |
6372
6373
  | `library(freeze)` | `freeze/2` | Delayed goals |
6373
6374
  | `library(gensym)` | `gensym/2`, `reset_gensym/1` | Process-local generated atoms |
package/why-eyeprolog.md CHANGED
@@ -143,8 +143,9 @@ This avoids a heuristic silently changing answer multiplicity, termination, or
143
143
  resource behavior for an otherwise ordinary recursive predicate.
144
144
 
145
145
  Forward chaining is explicit too. Normal mode recognizes `Conclusion :+ Premise`
146
- and executes those rules with a native fixed-point driver, including Eyelet's
147
- `true :+ Goal` query and `false :+ Goal` fuse conventions. Strict ISO mode
146
+ and executes those rules with a native fixed-point driver. The bundled
147
+ `library(eyelet)` exports the `:+` operator, `stable/1`, and `becomes/2`, including
148
+ Eyelet's `true :+ Goal` query and `false :+ Goal` fuse conventions. Strict ISO mode
148
149
  removes the operator. Because resource exhaustion is not logical failure, normal
149
150
  execution has no hidden depth cutoff; an explicitly requested depth limit raises
150
151
  `resource_error(depth_limit)`.