eyeprolog 1.5.2 → 1.5.4

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
@@ -31,8 +31,6 @@ Run EyeProlog without installing it globally:
31
31
 
32
32
  ```sh
33
33
  npx --yes eyeprolog
34
- ?- use_module(library(lists)).
35
- true.
36
34
  ?- member(X, [prolog, logic]).
37
35
  X = prolog
38
36
  ; X = logic.
@@ -59,11 +57,13 @@ printf 'human(socrates).\nmortal(X) :- human(X).\n' |
59
57
  ```
60
58
 
61
59
  ## Native forward rules (`:+`)
62
-
63
60
  Normal mode executes top-level `Conclusion :+ Premise` rules natively when no explicit
64
61
  `-g/--goal` is supplied. `true :+ Goal` prints answers and `false :+ Goal` emits a
65
- `fuse/1`; JavaScript `run()` follows the same rule. Strict ISO mode disables this
66
- extension. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
62
+ `fuse/1`; JavaScript `run()` follows the same rule. The bundled `library(eyelet)`
63
+ exports the `:+` operator, `stable/1`, and `becomes/2`. Strict ISO mode disables this
64
+ extension. Bundled `src/lib/` predicates autoload in files, CLI/API goals, and the REPL;
65
+ use `--no-autoload` to require explicit imports, and explicitly import libraries that introduce
66
+ operators before using their syntax. See [*The Art of EyeProlog*](the-art-of-eyeprolog.md) for the full semantics.
67
67
 
68
68
  ## Links
69
69
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.2",
6
+ "version": "1.5.4",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/execute.js CHANGED
@@ -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',
@@ -121,7 +121,7 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
121
121
  | 5.5.6 Side effects | Normal mode adds `statistics/0-2`, cleanup/library state, and optional proof/statistics host instrumentation. | The strict registry excludes Prolog-visible statistics/cleanup/library adapters; ordinary Part 1 I/O/database/flag/operator side effects remain. Host proof/statistics collection observes execution through the embedding API rather than adding a strict Prolog goal effect. **covered** — strict registry/execution regression. |
122
122
  | 5.5.7 Control constructs | `tnot/1` and normal-profile execution optimizations | `tnot/1` and the normal-profile explicit `table` declaration are absent. |
123
123
  | 5.5.8 Flags | `occurs_check` | Absent in strict mode. |
124
- | 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and interop autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
124
+ | 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and bundled-library autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
125
125
  | 5.5.10 Evaluable functors | Normal mode additionally accepts the EyeProlog evaluable atom `e`; the remaining arithmetic functors accepted by strict mode are the Part 1 + Corrigenda set. | Strict mode rejects `e/0` as non-evaluable and retains the Corrigendum arithmetic additions. **covered** — strict extension-boundary regression plus `src/iso-arithmetic.js`. |
126
126
  | 5.5.11 Reserved atoms | None | None. |
127
127
  | Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` options `double_quotes(true|false)` and `spacing(true|false)`. `spacing(false)` emits only lexically required separators; `spacing(true)` adds conventional layout around operators. Strict core excludes these implementation-specific extensions and accepts only the Part 1 plus Corrigendum 3 write-option surface; unknown extension options raise `domain_error(write_option,...)`. |
@@ -4784,6 +4784,35 @@ function documentationSyncCases() {
4784
4784
  assertEqual(result.stderr, '', 'stderr');
4785
4785
  },
4786
4786
  },
4787
+ {
4788
+ name: 'reference docs match explicit tabling and current runtime extensions',
4789
+ run: () => {
4790
+ const readme = fs.readFileSync(path.join(packageRoot, 'README.md'), 'utf8');
4791
+ const book = fs.readFileSync(path.join(packageRoot, 'the-art-of-eyeprolog.md'), 'utf8');
4792
+ const profile = fs.readFileSync(path.join(packageRoot, 'why-eyeprolog.md'), 'utf8');
4793
+ const combined = `${readme}
4794
+ ${book}
4795
+ ${profile}`;
4796
+ for (const stale of [
4797
+ 'Automatic tabling',
4798
+ 'Automatic hybrid reasoning',
4799
+ 'eligible positive recursive groups are tabled automatically',
4800
+ '--no-auto-table',
4801
+ 'conservative interop autoloading',
4802
+ ]) assertNotIncludes(combined, stale, `stale documentation phrase ${stale}`);
4803
+ assertIncludes(book, 'including recursive calls, use depth-first resolution unless the source', 'ordinary recursion is depth-first');
4804
+ assertIncludes(book, 'explicitly declares `:- table p/n.`', 'tabling is explicit');
4805
+ assertIncludes(book, 'The autoload index covers every', 'generic bundled-library autoload');
4806
+ assertIncludes(book, 'interactive top-level query therefore autoloads its canonical', 'REPL autoload');
4807
+ assertIncludes(book, 'Autoloading therefore supplies', 'autoload syntax boundary');
4808
+ assertIncludes(book, 'Residual constraints are part of the displayed answer even when', 'top-level hidden residuals');
4809
+ assertIncludes(book, 'EyeProlog normal mode also accepts `:+`', 'native forward-rule extension');
4810
+ assertIncludes(book, '`library(eyelet)`', 'Eyelet library surface');
4811
+ assertIncludes(readme, '`library(eyelet)`', 'README Eyelet library surface');
4812
+ assertIncludes(profile, 'an unresolved unqualified predicate may autoload its unique provider', 'Why EyeProlog autoload policy');
4813
+ assertIncludes(readme, 'Bundled `src/lib/` predicates autoload in files, CLI/API goals,', 'README autoload summary');
4814
+ },
4815
+ },
4787
4816
  {
4788
4817
  name: 'WG17 syntax status matches its executable-coverage manifest',
4789
4818
  run: () => {
@@ -5442,6 +5471,20 @@ function apiCases() {
5442
5471
  assertEqual(result.haltCode, null, 'forward halt code');
5443
5472
  },
5444
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
+ },
5445
5488
  {
5446
5489
  name: 'run exposes false/0 as an always-failing built-in',
5447
5490
  run: () => {
@@ -6260,7 +6303,7 @@ answer(ok) :-
6260
6303
  assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
6261
6304
  assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
6262
6305
  assertEqual(registeredNativeEyePrologLibraryNames().length, 58, 'public host-supported EyeProlog library count');
6263
- assertEqual(eyePrologPortableLibraryIndicators.length, 241, 'portable Prolog library count');
6306
+ assertEqual(eyePrologPortableLibraryIndicators.length, 243, 'portable Prolog library count');
6264
6307
  assertEqual(eyePrologInteropLibraryIndicators.length, 189, 'cross-implementation interop profile count');
6265
6308
  assertEqual(eyePrologInteropLibraryModules.length, 23, 'common explicit library module profile count');
6266
6309
  assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
@@ -6272,11 +6315,13 @@ answer(ok) :-
6272
6315
  assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'set_nth0/4 is outside the conservative interop subset');
6273
6316
  assertEqual(eyePrologLibraryAutoload['set_nth0/4'], 'lists', 'complete library autoload includes EyeProlog-only exports');
6274
6317
  assertEqual(eyePrologLibraryAutoload['pairs_keys_values/3'], 'pairs', 'complete library autoload includes library(pairs)');
6275
- 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');
6276
6321
  assertEqual(eyePrologNativeLibraryIndicators.length, 58, 'host-supported library count');
6277
6322
  assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
6278
6323
  assertEqual(eyePrologNativeLibraryIndicators.includes('random/1'), true, 'stateful random/1 is classified as host-supported');
6279
- assertEqual(eyePrologLibraryIndicators.length, 299, 'complete non-ISO EyeProlog library surface');
6324
+ assertEqual(eyePrologLibraryIndicators.length, 301, 'complete non-ISO EyeProlog library surface');
6280
6325
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
6281
6326
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
6282
6327
  assertEqual(Boolean(library.get('eyeprolog__call_residue_vars', 2)), true, 'private call_residue_vars adapter is registered for EyeProlog');
@@ -30,7 +30,7 @@ program with EyeProlog means learning to move comfortably between these views.
30
30
 
31
31
  EyeProlog implements a broad ISO Prolog profile with facts, clauses, terms, lists,
32
32
  control, arithmetic, dynamic predicates, operators, streams, and standard
33
- built-ins. Automatic
33
+ built-ins. Explicit
34
34
  tabling, explicit integrity checks, and proof output are implementation
35
35
  capabilities around that standards-based foundation. EyeProlog does not attempt to
36
36
  claim formal certification of every ISO processor edge case.
@@ -175,7 +175,8 @@ Approach each example through the same six moves:
175
175
  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
- 17 to see where EyeProlog's hybrid execution and proof-oriented design differ.
178
+ 17 to see how EyeProlog combines ordinary depth-first Prolog with explicit
179
+ tabling, native forward rules, and inspectable proofs.
179
180
  Chapter 41 gives further routes through the material.
180
181
 
181
182
  ### When a run surprises you
@@ -1680,16 +1681,16 @@ is a forward rule. When a loaded program contains such rules and no explicit
1680
1681
  top-level goal overrides them, the engine runs a native closure driver rather
1681
1682
  than a Prolog meta-interpreter. It repeatedly solves premises against the
1682
1683
  current program, asserts novel conjuncts from successful conclusions, and
1683
- continues until no new conclusion is added. This is the execution path used by
1684
- 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.
1685
1686
 
1686
1687
  Two conclusions have control meaning. `true :+ Goal` is a query and prints each
1687
1688
  distinct successful instance of `Goal`. `false :+ Goal` is an integrity fuse:
1688
1689
  on success EyeProlog prints `fuse(Goal)` and returns halt status 2. Variables
1689
1690
  that occur only in an ordinary derived conclusion are existential and become
1690
1691
  `sk_0`, `sk_1`, and so on; a derived conclusion that is itself a `:+` rule keeps
1691
- its variables universal. Eyelet's `stable/1` and `becomes/2` remain library-level
1692
- 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.
1693
1694
 
1694
1695
  The JavaScript convenience `run()` function selects this forward mode when no
1695
1696
  explicit `goal` or `goals` option is supplied. Advanced embedders can inspect a
@@ -5685,10 +5686,11 @@ so `edge/2` and `edge/3` are different predicates.
5685
5686
 
5686
5687
  Execution is goal-directed rather than complete bottom-up saturation. Goals in
5687
5688
  a body normally run from left to right; the solver may select a ready
5688
- deterministic built-in early as a pure filter. Ordinary user-defined calls use
5689
- depth-first resolution, while eligible positive recursive groups are tabled
5690
- automatically. `\+/1` is stratified negation as failure, not classical
5691
- negation.
5689
+ deterministic built-in early as a pure filter. Ordinary user-defined calls,
5690
+ including recursive calls, use depth-first resolution unless the source
5691
+ explicitly declares `:- table p/n.`. `\+/1` is negation as failure, not
5692
+ classical negation; the separate `tnot/1` extension provides well-founded
5693
+ semantics for eligible finite Datalog components.
5692
5694
 
5693
5695
  EyeProlog supports cut, operator declarations, dynamic database updates, grouped
5694
5696
  solutions, exceptions, flags, initialization and inclusion directives, and
@@ -5840,21 +5842,27 @@ time. Source facts are not echoed as new conclusions, and duplicate answers
5840
5842
  are suppressed. Answers are not asserted back into the running program.
5841
5843
  Supported output syntax is designed to be readable as Prolog input accepted by EyeProlog.
5842
5844
 
5843
- #### Automatic hybrid reasoning
5845
+ #### Explicit tabling and recursion planning
5844
5846
 
5845
- The program loader detects predicate-dependency cycles, including dependencies
5846
- inside conjunction, `\+/1`, `once/1`, and aggregate goals.
5847
- Positive recursive components—including directly queried recursive
5848
- relations—are tabled to an answer fixed point before answers are replayed.
5849
- Components with a negative dependency retain guarded ordinary resolution,
5850
- because positive least-fixed-point tabling does not define unstratified
5851
- negation. Nonrecursive groups use indexed, depth-first resolution.
5847
+ The program loader analyzes predicate dependencies and recursion so the solver
5848
+ can choose semantics-preserving indexes and fast paths. That analysis does
5849
+ **not** decide whether a predicate is tabled. Ordinary predicates—including
5850
+ recursive ones—use indexed, depth-first Prolog resolution unless their source
5851
+ explicitly declares `:- table p/n.`.
5852
5852
 
5853
- For calls with ground structural input, tabled answers can be reused within a
5854
- solver run. The engine infers common structurally decreasing inputs from
5855
- recursive heads. Fully open calls and calls whose inferred structural input is
5856
- not ground may remain under ordinary resolution rather than forcing a possibly
5857
- infinite relation into a table. This changes control, not declarative meaning.
5853
+ An explicitly tabled positive recursive predicate is coordinated through an
5854
+ answer table: recurring calls consume answers already found, new answers are
5855
+ recorded, and evaluation continues toward a fixed point. For eligible large,
5856
+ finite, function-free Datalog components, EyeProlog may represent that declared
5857
+ table as one shared most-general relation or an indexed least model. For other
5858
+ declared tables the engine may infer structurally bound input positions to
5859
+ improve table reuse. These are implementation choices inside an explicit table
5860
+ declaration; they never opt an undeclared predicate into tabling.
5861
+
5862
+ Ordinary `\+/1` remains ISO-style negation as failure. The separate `tnot/1`
5863
+ extension explicitly requests well-founded evaluation for eligible finite,
5864
+ range-restricted Datalog dependencies. Strict ISO mode exposes neither `table`
5865
+ nor `tnot/1`.
5858
5866
 
5859
5867
  #### Query execution
5860
5868
 
@@ -5872,8 +5880,8 @@ answers. The host:
5872
5880
  7. prints each answer and, only when requested, its `why/2` explanation.
5873
5881
 
5874
5882
  Goal selection affects host execution rather than the program's logical meaning.
5875
- One goal's answers are not asserted for later goals, although internal
5876
- tables may be reused during the solver run. For stable output, queries for
5883
+ One goal's answers are not asserted for later goals, although explicitly
5884
+ declared tables may be reused during the solver run. For stable output, queries for
5877
5885
  known predicates are grouped by the source order in which their predicate
5878
5886
  groups first appear; goals within one group retain their supplied order.
5879
5887
  Queries for predicates with no group follow the known groups.
@@ -6270,12 +6278,12 @@ so side effects occur in Prolog execution order.
6270
6278
 
6271
6279
  ### The EyeProlog library
6272
6280
 
6273
- EyeProlog exposes **299 distinct non-ISO library and normal-extension predicate
6281
+ EyeProlog exposes **301 distinct non-ISO library and normal-extension predicate
6274
6282
  indicators** in addition to the 129 indicators in its isolated ISO profile.
6275
- **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
6276
6284
  `src/lib/`; **58 use host support** for control, attributed variables,
6277
6285
  constraints, or observability. The ISO and library catalogs therefore cover
6278
- **428 distinct predicate indicators**. Normal-mode controls and observability
6286
+ **430 distinct predicate indicators**. Normal-mode controls and observability
6279
6287
  relations that do not belong to a library, such as `call_cleanup/2`,
6280
6288
  `setup_call_cleanup/3`, `statistics/0,2`, and `tnot/1`, are documented in their
6281
6289
  own sections rather than counted as library predicates. `time/1` is additionally
@@ -6288,7 +6296,7 @@ The sources are `src/lib/aggregate.pl`, `src/lib/arithmetic.pl`,
6288
6296
  `src/lib/charsio.pl`, `src/lib/clpb.pl`, `src/lib/clpz.pl`,
6289
6297
  `src/lib/comparison.pl`, `src/lib/dates.pl`, `src/lib/dcgs.pl`,
6290
6298
  `src/lib/debug.pl`, `src/lib/dif.pl`, `src/lib/error.pl`,
6291
- `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`,
6292
6300
  `src/lib/iso_ext.pl`, `src/lib/lambda.pl`, `src/lib/lists.pl`,
6293
6301
  `src/lib/ordsets.pl`, `src/lib/pairs.pl`, `src/lib/pio.pl`,
6294
6302
  `src/lib/primes.pl`, `src/lib/prologue.pl`, `src/lib/random.pl`,
@@ -6360,6 +6368,7 @@ bindings make one aligned subterm pair sufficient. For example:
6360
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 |
6361
6369
  | `library(dif)` | `dif/2` | Common module facade over native delayed disequality |
6362
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 |
6363
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 |
6364
6373
  | `library(freeze)` | `freeze/2` | Delayed goals |
6365
6374
  | `library(gensym)` | `gensym/2`, `reset_gensym/1` | Process-local generated atoms |
@@ -7475,7 +7484,7 @@ recursion as one technique.
7475
7484
  | [Deep taxonomy: 10](https://github.com/eyereasoner/eyeprolog/blob/main/examples/deep-taxonomy-10.pl) | A small generated hierarchy is readable by hand and establishes the benchmark shape. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/deep-taxonomy-10.pl) |
7476
7485
  | [Deep Taxonomy 100](https://github.com/eyereasoner/eyeprolog/blob/main/examples/deep-taxonomy-100.pl) | A 100-step taxonomy chain that exercises deep recursive closure and side-label derivation. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/deep-taxonomy-100.pl) |
7477
7486
  | [Deep taxonomy: 1,000](https://github.com/eyereasoner/eyeprolog/blob/main/examples/deep-taxonomy-1000.pl) | The same logical theory tests indexing and recursive closure at a realistic depth. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/deep-taxonomy-1000.pl) |
7478
- | [Deep Taxonomy 10000](https://github.com/eyereasoner/eyeprolog/blob/main/examples/deep-taxonomy-10000.pl) | A 10,000-step taxonomy chain used as a large-depth tabling and closure stress test. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/deep-taxonomy-10000.pl) |
7487
+ | [Deep Taxonomy 10000](https://github.com/eyereasoner/eyeprolog/blob/main/examples/deep-taxonomy-10000.pl) | A 10,000-step taxonomy chain used as a large-depth ordinary-recursion and closure stress test. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/deep-taxonomy-10000.pl) |
7479
7488
  | [Deep taxonomy: 100,000](https://github.com/eyereasoner/eyeprolog/blob/main/examples/deep-taxonomy-100000.pl) | A stress case separates semantic simplicity from implementation scale. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/deep-taxonomy-100000.pl) |
7480
7489
  | [Family cousins](https://github.com/eyereasoner/eyeprolog/blob/main/examples/family-cousins.pl) | Several relational joins derive kinship beyond a simple transitive closure. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/family-cousins.pl) |
7481
7490
  | [Graph reachability](https://github.com/eyereasoner/eyeprolog/blob/main/examples/graph-reachability.pl) | A visited list bounds cyclic traversal and makes explicit negative test cases finite. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/graph-reachability.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/graph-reachability.pl) |
package/why-eyeprolog.md CHANGED
@@ -143,12 +143,21 @@ 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)`.
151
152
 
153
+ Library convenience is explicit at the profile boundary too. Outside strict ISO
154
+ mode, an unresolved unqualified predicate may autoload its unique provider from
155
+ the bundled `src/lib/` modules after program definitions, standard built-ins,
156
+ and explicit imports have had precedence. `--no-autoload` disables this
157
+ convenience, and strict ISO mode disables it unconditionally. Autoloading happens
158
+ after parsing, so libraries that introduce operators still require an explicit
159
+ `use_module/1-2` before that syntax is read.
160
+
152
161
  Definite clause grammars follow the ISO Part 3 difference-list model. Internal
153
162
  fast paths make deep finite sequence processing economical while preserving
154
163
  relational modes. The checked