eyeprolog 1.3.65 → 1.3.66

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
@@ -130,6 +130,23 @@ local/global variable stack. `phrase/2` likewise supplies its fixed `[]`
130
130
  remainder directly to the grammar; `phrase/3` retains its delayed final output
131
131
  unification for steadfastness.
132
132
 
133
+ ### Delayed disequality with `dif/2`
134
+
135
+ The normal EyeProlog runtime provides `dif/2` as a conforming extension;
136
+ `--iso-strict` remains limited to ISO/IEC 13211-1 and does not expose it.
137
+ Unlike `\=/2`, which only tests whether two terms are non-unifiable *now*,
138
+ `dif/2` can succeed with a residual disequality constraint and reject a later
139
+ binding that would make the terms identical. For example,
140
+ `dif(X, Y), X = Y` fails, while `dif(X-Y, 1-2), X = Y, Y = 1` succeeds once
141
+ the structural constraint becomes provably satisfied.
142
+
143
+ The implementation uses backtracking-safe annotated variables in `Env`. A
144
+ pending constraint is indexed by every currently unbound variable it mentions;
145
+ variable aliasing and later bindings reindex the annotation, entailed constraints
146
+ are removed, and violated constraints make unification fail. The interactive
147
+ top level includes pending `dif/2` goals in residual answers. See
148
+ [`examples/dif-constraints.pl`](examples/dif-constraints.pl).
149
+
133
150
  Recursion through negation is explicit. EyeProlog provides `tnot/1` for
134
151
  well-founded negation over finite, range-restricted, function-free Datalog
135
152
  components. Ordinary `\+/1` remains negation-as-failure and is not silently
@@ -258,7 +275,7 @@ be continued without rebuilding a full clause-resolution frame for every
258
275
  suffix. Open and remainder-producing uses still retain the ordinary DCG
259
276
  solutions and backtracking behavior.
260
277
 
261
- EyeProlog also adds 128 public library predicate indicators to its 129-entry ISO
278
+ EyeProlog also adds 129 public library predicate indicators to its 129-entry ISO
262
279
  profile. **88 are implemented entirely as ordinary Prolog clauses** in focused
263
280
  modules under `src/lib/`; the remaining control predicates and finite-domain
264
281
  `library(clpz)` kernel use backtrackable host support.
@@ -0,0 +1,19 @@
1
+ % Delayed disequality constraints with dif/2.
2
+ %
3
+ % Unlike \=/2, dif/2 records a constraint when its arguments are still
4
+ % unifiable and rechecks that constraint after later bindings.
5
+
6
+ %% goal: allowed_pair(X, Y)
7
+ %% goal: specialization(X, Y)
8
+
9
+ allowed_pair(X, Y) :-
10
+ dif(X, Y),
11
+ X = left,
12
+ Y = right.
13
+
14
+ % The structural constraint remains pending after X=Y. Binding both to 1
15
+ % then proves X-Y and 1-2 different, so the constraint is discharged.
16
+ specialization(X, Y) :-
17
+ dif(X-Y, 1-2),
18
+ X = Y,
19
+ Y = 1.
@@ -0,0 +1,2 @@
1
+ allowed_pair(left, right).
2
+ specialization(1, 1).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.65",
6
+ "version": "1.3.66",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/playground.html CHANGED
@@ -495,6 +495,7 @@
495
495
  "derived-backward-rule",
496
496
  "derived-rule",
497
497
  "diamond-property",
498
+ "dif-constraints",
498
499
  "dijkstra",
499
500
  "dijkstra-findall-sort",
500
501
  "dijkstra-risk-path",
@@ -31,6 +31,13 @@ kernel component into the ISO registry or a frontend. In particular,
31
31
  report processor errors without importing `iso.js` and creating an
32
32
  `iso.js <-> dcg.js` cycle.
33
33
 
34
+ `term.js` also owns the generic annotated-variable storage carried by `Env`.
35
+ Annotations are persistent across `Env.clone()` and therefore backtrack with the
36
+ substitution. Language services may attach immutable constraint descriptors,
37
+ but the unconstrained unification hot path only performs a null check; descriptor
38
+ validation and reindexing run only in environments that actually contain
39
+ annotations. `dif/2` is the first user of this mechanism.
40
+
34
41
  The JavaScript runtime stays flat directly under `src/`; the existing `src/lib/`
35
42
  contains Prolog library sources rather than JavaScript runtime modules. The architecture
36
43
  test rejects JavaScript import cycles under `src/`. Cleanup lifecycle hooks are
package/src/iso.js CHANGED
@@ -167,6 +167,7 @@ export const eyePrologLibraryBuiltins = {
167
167
  registry.add('eyeprolog__call_nth', 2, callNthBuiltin, { eyePrologLibrary: true });
168
168
  registry.add('eyeprolog__countall', 2, countAllBuiltin, { eyePrologLibrary: true });
169
169
  registry.add('eyeprolog__freeze', 2, freezeBuiltin, { eyePrologLibrary: true });
170
+ registry.add('dif', 2, difBuiltin, { deterministic: true, eyePrologLibrary: true });
170
171
  registry.add('time', 1, timeBuiltin, { eyePrologLibrary: true });
171
172
  registry.add('eyeprolog__time', 1, timeBuiltin, { eyePrologLibrary: true });
172
173
  },
@@ -189,6 +190,43 @@ function* nonUnification({ goal, env }) {
189
190
  if (!unify(goal.args[0], goal.args[1], env.clone())) yield env;
190
191
  }
191
192
 
193
+ function difConstraint(left, right) {
194
+ return Object.freeze({
195
+ kind: 'dif',
196
+ left,
197
+ right,
198
+ variables(env) {
199
+ return new Set([...termVariableNames(left, env), ...termVariableNames(right, env)]);
200
+ },
201
+ status(env) {
202
+ if (identical(left, right, env)) return 'violated';
203
+ const probe = env.clone();
204
+ return unify(left, right, probe, { skipVariableConstraints: true }) ? 'pending' : 'entailed';
205
+ },
206
+ residualGoal() {
207
+ return compound('dif', [left, right]);
208
+ },
209
+ });
210
+ }
211
+
212
+ function* difBuiltin({ goal, env }) {
213
+ const left = goal.args[0];
214
+ const right = goal.args[1];
215
+ if (identical(left, right, env)) return;
216
+
217
+ // In EyeProlog's finite-tree model, terms that cannot unify are already
218
+ // provably different and need no residual constraint. Otherwise attach a
219
+ // disequality constraint to every currently unbound variable it mentions.
220
+ const probe = env.clone();
221
+ if (!unify(left, right, probe, { skipVariableConstraints: true })) {
222
+ yield env;
223
+ return;
224
+ }
225
+ const next = env.clone();
226
+ next.addVariableConstraint(difConstraint(left, right));
227
+ yield next;
228
+ }
229
+
192
230
  function termVariableNames(term, env, names = new Set(), seen = new Set()) {
193
231
  term = deref(term, env);
194
232
  if (term.type === VAR) {
@@ -1646,7 +1684,7 @@ function writeOptionBoolean(value, env, option) {
1646
1684
  value = deref(value, env);
1647
1685
  if (value.type === VAR) throw new PrologError('instantiation_error');
1648
1686
  if (value.type !== ATOM || !['true', 'false'].includes(value.name)) {
1649
- throw new PrologError('domain_error(write_option)', option);
1687
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1650
1688
  }
1651
1689
  return value.name === 'true';
1652
1690
  }
@@ -1657,19 +1695,20 @@ function writeVariableNames(value, env, option) {
1657
1695
  const items = properListItems(value, env);
1658
1696
  if (items == null) {
1659
1697
  if (isPartialList(value, env)) throw new PrologError('instantiation_error');
1660
- throw new PrologError('domain_error(write_option)', option);
1698
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1661
1699
  }
1662
1700
  const names = new Map();
1663
1701
  for (const item of items) {
1664
1702
  const pair = deref(item, env);
1703
+ if (pair.type === VAR) throw new PrologError('instantiation_error');
1665
1704
  if (pair.type !== COMPOUND || pair.name !== '=' || pair.arity !== 2) {
1666
- throw new PrologError('domain_error(write_option)', option);
1705
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1667
1706
  }
1668
1707
  const name = deref(pair.args[0], env);
1669
1708
  const target = deref(pair.args[1], env);
1670
1709
  if (name.type === VAR) throw new PrologError('instantiation_error');
1671
1710
  if (name.type !== ATOM) {
1672
- throw new PrologError('domain_error(write_option)', option);
1711
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1673
1712
  }
1674
1713
  // Corrigendum 3 permits any term on the right. Only variables can name a
1675
1714
  // variable being written; retain the leftmost applicable entry.
@@ -1682,7 +1721,7 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
1682
1721
  const result = defaultTermWriteOptions(mode);
1683
1722
  for (const option of options) {
1684
1723
  if (option.type !== COMPOUND || option.arity !== 1) {
1685
- throw new PrologError('domain_error(write_option)', option);
1724
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1686
1725
  }
1687
1726
  if (option.name === 'quoted') result.quoted = writeOptionBoolean(option.args[0], env, option);
1688
1727
  else if (option.name === 'ignore_ops') result.ignoreOps = writeOptionBoolean(option.args[0], env, option);
@@ -1691,7 +1730,7 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
1691
1730
  else if (option.name === 'double_quotes' && !solver?.isoStrict) {
1692
1731
  result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
1693
1732
  } else {
1694
- throw new PrologError('domain_error(write_option)', option);
1733
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1695
1734
  }
1696
1735
  }
1697
1736
  return result;
package/src/repl.js CHANGED
@@ -740,6 +740,19 @@ function formatAnswer(engine, state, variables, env) {
740
740
  doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
741
741
  })}`);
742
742
  }
743
+ for (const constraint of env.variableConstraints?.() ?? []) {
744
+ const residual = constraint.residualGoal?.(env);
745
+ if (residual == null) continue;
746
+ collectUnboundVariables(engine, residual, env, names, () => `_${letterName(generated++)}`);
747
+ bindings.push(engine.formatTermForWrite(residual, env, {
748
+ quoted: true,
749
+ operators,
750
+ variableNames: names,
751
+ operatorAtomsAsArgs: true,
752
+ dottedGraphicAtoms: true,
753
+ doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
754
+ }));
755
+ }
743
756
  return bindings.length === 0 ? 'true' : bindings.join(', ');
744
757
  }
745
758
 
package/src/solver.js CHANGED
@@ -1971,7 +1971,7 @@ function fastCountBranch(branch, env) {
1971
1971
 
1972
1972
  function fastCountPureGoal(solver, goal, env) {
1973
1973
  if (solver.isoStrict || solver.solutionLimit !== Infinity || solver.maxInferences !== Infinity ||
1974
- env._clpz != null || (env._delays != null && env._delays.size !== 0)) return null;
1974
+ env._clpz != null || env._variableConstraints != null || (env._delays != null && env._delays.size !== 0)) return null;
1975
1975
  const budget = { remaining: 8192 };
1976
1976
  const branches = expandPureCountGoal(solver, goal, goal.module ?? 'user', new Set(), budget);
1977
1977
  if (branches == null) return null;
@@ -2015,6 +2015,7 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
2015
2015
  if (state.index === runLength) {
2016
2016
  const next = env.clone();
2017
2017
  for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
2018
+ if (next._variableConstraints != null && !next.validateVariableConstraints()) continue;
2018
2019
  frames.push({ kind: 'goals', goals: rest, env: next, depth: state.depth, active });
2019
2020
  if (frames.length > frameLimit) {
2020
2021
  // Do not repeat the old bug of abandoning the optimization and then
@@ -2079,6 +2080,7 @@ function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
2079
2080
  if (state.index === goals.length) {
2080
2081
  const next = env.clone();
2081
2082
  for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
2083
+ if (next._variableConstraints != null && !next.validateVariableConstraints()) continue;
2082
2084
  yield next;
2083
2085
  continue;
2084
2086
  }
@@ -2183,6 +2185,7 @@ function matchScalarFact(goal, head, env) {
2183
2185
 
2184
2186
  const next = env.clone();
2185
2187
  for (let i = 0; i < names.length; i++) next.bind(names[i], values[i]);
2188
+ if (next._variableConstraints != null && !next.validateVariableConstraints()) return null;
2186
2189
  return next;
2187
2190
  }
2188
2191
 
@@ -48,7 +48,7 @@ function libraryUrl(filename) {
48
48
  }
49
49
 
50
50
  export const eyePrologNativeLibraryIndicators = Object.freeze([
51
- 'call_nth/2', 'freeze/2', 'countall/2', 'time/1',
51
+ 'call_nth/2', 'freeze/2', 'dif/2', 'countall/2', 'time/1',
52
52
  '#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
53
53
  '#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
54
54
  'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
package/src/term.js CHANGED
@@ -97,6 +97,11 @@ export class Env {
97
97
  cache: null,
98
98
  };
99
99
  this._delays = null;
100
+ // Backtracking-safe attributed-variable constraints. Constraints are
101
+ // immutable descriptors shared by cloned environments; the Set/Map index
102
+ // is copied only when a branch adds, removes, or reindexes a constraint.
103
+ this._variableConstraints = null;
104
+ this._variableAnnotations = null;
100
105
  this._clpz = null;
101
106
  this._occursCheckHandler = null;
102
107
  this._localVariables = null;
@@ -110,6 +115,8 @@ export class Env {
110
115
  const clone = Object.create(Env.prototype);
111
116
  clone._state = this._state;
112
117
  clone._delays = this._delays;
118
+ clone._variableConstraints = this._variableConstraints;
119
+ clone._variableAnnotations = this._variableAnnotations;
113
120
  clone._clpz = this._clpz;
114
121
  clone._occursCheckHandler = this._occursCheckHandler;
115
122
  clone._localVariables = this._localVariables;
@@ -236,6 +243,54 @@ export class Env {
236
243
  if (ready.length > 0) this._delays = remaining;
237
244
  return ready;
238
245
  }
246
+ addVariableConstraint(constraint) {
247
+ if (constraint == null || typeof constraint.variables !== 'function' ||
248
+ typeof constraint.status !== 'function') {
249
+ throw new TypeError('variable constraint requires variables(env) and status(env)');
250
+ }
251
+ const constraints = new Set(this._variableConstraints ?? []);
252
+ constraints.add(constraint);
253
+ this._variableConstraints = constraints;
254
+ this._reindexVariableConstraints();
255
+ }
256
+ variableConstraints(kind = null) {
257
+ const constraints = [...(this._variableConstraints ?? [])];
258
+ return kind == null ? constraints : constraints.filter((constraint) => constraint.kind === kind);
259
+ }
260
+ variableAnnotations(name) {
261
+ const root = deref(variable(name), this);
262
+ if (root.type !== VAR) return [];
263
+ return [...(this._variableAnnotations?.get(root.name) ?? [])];
264
+ }
265
+ validateVariableConstraints() {
266
+ if (this._variableConstraints == null || this._variableConstraints.size === 0) return true;
267
+ const pending = new Set();
268
+ for (const constraint of this._variableConstraints) {
269
+ const status = constraint.status(this);
270
+ if (status === 'violated') return false;
271
+ if (status !== 'entailed') pending.add(constraint);
272
+ }
273
+ this._variableConstraints = pending.size === 0 ? null : pending;
274
+ this._reindexVariableConstraints();
275
+ return true;
276
+ }
277
+ _reindexVariableConstraints() {
278
+ if (this._variableConstraints == null || this._variableConstraints.size === 0) {
279
+ this._variableAnnotations = null;
280
+ return;
281
+ }
282
+ const annotations = new Map();
283
+ for (const constraint of this._variableConstraints) {
284
+ for (const name of constraint.variables(this)) {
285
+ const root = deref(variable(name), this);
286
+ if (root.type !== VAR) continue;
287
+ const set = annotations.get(root.name) ?? new Set();
288
+ set.add(constraint);
289
+ annotations.set(root.name, set);
290
+ }
291
+ }
292
+ this._variableAnnotations = annotations.size === 0 ? null : annotations;
293
+ }
239
294
  }
240
295
 
241
296
  export function deref(term, env) {
@@ -373,6 +428,7 @@ export function unify(left, right, env, options = {}) {
373
428
 
374
429
  return false;
375
430
  }
431
+ if (options.skipVariableConstraints !== true && env?._variableConstraints != null && !env.validateVariableConstraints()) return false;
376
432
  return true;
377
433
  }
378
434
 
@@ -110,6 +110,41 @@ export function runRegression(reporter = new TestReporter(), requestedSection =
110
110
 
111
111
  function regressionCases() {
112
112
  return [
113
+ {
114
+ name: 'dif/2 keeps a delayed disequality across later unification (issue #68)',
115
+ run: () => {
116
+ assertEqual(run('', { goal: 'dif(1,Y),Y=1' }).stats.completed_goal_lists, 0,
117
+ 'binding a constrained variable to the forbidden value fails');
118
+ assertEqual(run('', { goal: 'dif(1,Y),Y=2' }).stats.completed_goal_lists, 1,
119
+ 'binding a constrained variable to a different value succeeds');
120
+ assertEqual(run('', { goal: 'dif(X,Y),X=Y' }).stats.completed_goal_lists, 0,
121
+ 'aliasing two constrained variables fails');
122
+ assertEqual(run('', { goal: 'dif(X-Y,1-2),X=Y,Y=1' }).stats.completed_goal_lists, 1,
123
+ 'a specialization that makes the terms non-unifiable discharges the constraint');
124
+ assertEqual(run('', { goal: 'dif(X-Y,1-2),X=Y,X=2' }).stats.completed_goal_lists, 1,
125
+ 'a second specialization discharges the same structural constraint');
126
+ assertEqual(run('p(a).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 0,
127
+ 'scalar-fact fast matching still validates annotated variables');
128
+ assertEqual(run('p(b).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 1,
129
+ 'scalar-fact fast matching can discharge an annotated disequality');
130
+ },
131
+ },
132
+ {
133
+ name: 'write_term variable_names/1 errors preserve the instantiated option culprit (issue #69)',
134
+ run: () => {
135
+ const scalar = run('', {
136
+ isoStrict: true,
137
+ goal: "catch((VN=1,write_term(T,[variable_names(VN)])),error(E,_),writeq(E))",
138
+ });
139
+ assertEqual(scalar.stdout, 'domain_error(write_option,variable_names(1))', 'scalar culprit');
140
+
141
+ const list = run('', {
142
+ isoStrict: true,
143
+ goal: "catch((VN=[[]],write_term(T,[variable_names(VN)])),error(E,_),writeq(E))",
144
+ });
145
+ assertEqual(list.stdout, 'domain_error(write_option,variable_names([[]]))', 'list culprit');
146
+ },
147
+ },
113
148
  {
114
149
  name: 'large source scanning avoids quadratic full-stop lookback',
115
150
  run: () => {
@@ -4694,7 +4729,7 @@ answer(ok) :-
4694
4729
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
4695
4730
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
4696
4731
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
4697
- assertEqual(library.defs.size, 160, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and private library adapters');
4732
+ assertEqual(library.defs.size, 161, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and private library adapters');
4698
4733
  assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
4699
4734
  assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
4700
4735
  assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
@@ -4705,11 +4740,13 @@ answer(ok) :-
4705
4740
  assertEqual(Boolean(library.get('tnot', 1)), true, 'tnot/1 is an EyeProlog WFS extension');
4706
4741
  assertEqual(registry.get('time', 1), null, 'time/1 is absent from the ISO registry');
4707
4742
  assertEqual(Boolean(library.get('time', 1)), true, 'time/1 is an EyeProlog timing extension');
4743
+ assertEqual(registry.get('dif', 2), null, 'dif/2 is absent from the strict ISO core registry');
4744
+ assertEqual(Boolean(library.get('dif', 2)), true, 'dif/2 is an EyeProlog attributed-variable constraint extension');
4708
4745
  assertEqual(registry.get('call_cleanup', 2), null, 'call_cleanup/2 is absent from the ISO registry');
4709
4746
  assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
4710
4747
  assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
4711
4748
  assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
4712
- assertEqual(registeredNativeEyePrologLibraryNames().length, 41, 'public native EyeProlog builtin count');
4749
+ assertEqual(registeredNativeEyePrologLibraryNames().length, 42, 'public native EyeProlog builtin count');
4713
4750
  assertEqual(eyePrologPortableLibraryIndicators.length, 87, 'portable Prolog library count');
4714
4751
  assertEqual(eyePrologInteropLibraryIndicators.length, 29, 'cross-implementation interop profile count');
4715
4752
  assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda', 'common explicit library module profile');
@@ -4719,9 +4756,9 @@ answer(ok) :-
4719
4756
  assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
4720
4757
  assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
4721
4758
  assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
4722
- assertEqual(eyePrologNativeLibraryIndicators.length, 41, 'native host library count');
4723
- assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
4724
- assertEqual(eyePrologLibraryIndicators.length, 128, 'complete EyeProlog library surface');
4759
+ assertEqual(eyePrologNativeLibraryIndicators.length, 42, 'native host library count');
4760
+ assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
4761
+ assertEqual(eyePrologLibraryIndicators.length, 129, 'complete EyeProlog library surface');
4725
4762
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
4726
4763
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
4727
4764
  assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
@@ -6211,13 +6211,13 @@ so side effects occur in Prolog execution order.
6211
6211
 
6212
6212
  ### The EyeProlog library
6213
6213
 
6214
- EyeProlog exposes **128 library predicate indicators** in addition to the 129
6214
+ EyeProlog exposes **129 library predicate indicators** in addition to the 129
6215
6215
  indicators in its isolated ISO profile. **88 are defined entirely as ordinary
6216
6216
  Prolog clauses** in focused modules under `src/lib/`. The remaining public
6217
6217
  relations use small private host adapters where control, constraints, or host
6218
6218
  observability cannot be expressed by ordinary clauses alone; `time/1` is one
6219
6219
  such relation. The resulting normal EyeProlog language surface is therefore
6220
- **257 public predicate indicators**. Most library relations remain module source
6220
+ **258 public predicate indicators**. Most library relations remain module source
6221
6221
  clauses over private adapters. `time/1` is additionally registered directly in
6222
6222
  the normal EyeProlog runtime so Trealla-style timing works at the interactive
6223
6223
  top level without an import; it is absent from the strict ISO registry.
@@ -6257,6 +6257,7 @@ between solution branches.
6257
6257
  | `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
6258
6258
  | `library(dates)` | `difference/3` |
6259
6259
  | `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2`, `time/1`, `.../2` |
6260
+ | Normal runtime extension | `dif/2` |
6260
6261
  | `library(lambda)` | `^/3`, `^/4`, `^/5`, `^/6`, `^/7`, `^/8`, `^/9`, `^/10`, `\/1`, `\/2`, `\/3`, `\/4`, `\/5`, `\/6`, `\/7`, `\/8`, `+\/2`, `+\/3`, `+\/4`, `+\/5`, `+\/6`, `+\/7`, `+\/8`, `+\/9` |
6261
6262
  | `library(lists)` | `member/2`, `memberchk/2`, `select/3`, `append/2`, `append/3`, `last/2`, `same_length/2`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `reverse/2`, `length/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `foldl/4`, `foldl/5`, `foldl/6`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` |
6262
6263
  | `library(primes)` | `smallest_divisor_from/3` |
@@ -7130,7 +7131,7 @@ Review questions:
7130
7131
  </figure>
7131
7132
 
7132
7133
  The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
7133
- top-level directory contains **211 self-contained runnable programs**. Every
7134
+ top-level directory contains **212 self-contained runnable programs**. Every
7134
7135
  source program has an exact answer file under
7135
7136
  [examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
7136
7137
  explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic tables below link every top-level program and open the program