eyeprolog 1.1.17 → 1.1.19

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
@@ -91,7 +91,7 @@ noun --> [world] | [prolog].
91
91
  %% goal: phrase(sentence, Words)
92
92
  ```
93
93
 
94
- EyeProlog also adds 91 public library predicate indicators to its 129-entry ISO
94
+ EyeProlog also adds 99 public library predicate indicators to its 129-entry ISO
95
95
  profile. **60 are implemented entirely as ordinary Prolog clauses** in focused
96
96
  modules under `src/lib/`; the control predicates and finite-domain
97
97
  `library(clpz)` kernel use backtrackable host support.
@@ -0,0 +1,35 @@
1
+ :- use_module(library(clpz)).
2
+
3
+ % Finite global constraints cover compatibility tables, lexicographic and
4
+ % non-overlapping schedules, value cardinalities with costs, Hamiltonian
5
+ % circuits, distinct-value counting, and three-way integer comparison.
6
+
7
+ %% goal: advanced_clpz(X0, X1)
8
+
9
+ advanced_clpz(table, compatible(4, Y, Distinct)) :-
10
+ tuples_in([[X, Y]], [[1, 2], [1, 5], [4, 0], [4, 3]]),
11
+ X = 4,
12
+ nvalue(Distinct, [X, Y]),
13
+ labeling([], [Y]).
14
+
15
+ advanced_clpz(schedule, starts([A, B, C])) :-
16
+ [A, B, C] ins 0..3,
17
+ lex_chain([[A, B], [B, C]]),
18
+ serialized([A, B, C], [1, 2, 1]),
19
+ A #< B,
20
+ B #< C,
21
+ labeling([ff], [A, B, C]).
22
+
23
+ advanced_clpz(cardinality, assignment(Values, Cost, Order)) :-
24
+ Values = [_, _, _],
25
+ global_cardinality(Values, [1-2, 3-1],
26
+ [cost(Cost, [[5, 1], [2, 4], [3, 6]])]),
27
+ labeling([ff], Values),
28
+ zcompare(Order, Cost, 10).
29
+
30
+ advanced_clpz(circuit, successors(Values)) :-
31
+ Values = [A, B, _, _],
32
+ circuit(Values),
33
+ A #= 2,
34
+ B #= 3,
35
+ labeling([ff], Values).
@@ -0,0 +1,64 @@
1
+ :- use_module(library(clpz)).
2
+
3
+ % AI Escargot is a well-known difficult 9-by-9 Sudoku. The model is entirely
4
+ % declarative: every row, column, and 3-by-3 block is all-distinct, while
5
+ % first-fail labeling chooses among the remaining finite-domain cells.
6
+
7
+ %% goal: sudoku9_solution(X0)
8
+
9
+ sudoku9_solution(Rows) :-
10
+ Rows = [
11
+ [1, _, _, _, _, 7, _, 9, _],
12
+ [_, 3, _, _, 2, _, _, _, 8],
13
+ [_, _, 9, 6, _, _, 5, _, _],
14
+ [_, _, 5, 3, _, _, 9, _, _],
15
+ [_, 1, _, _, 8, _, _, _, 2],
16
+ [6, _, _, _, _, 4, _, _, _],
17
+ [3, _, _, _, _, _, _, 1, _],
18
+ [_, 4, _, _, _, _, _, _, 7],
19
+ [_, _, 7, _, _, _, 3, _, _]
20
+ ],
21
+ sudoku9_rows(Rows),
22
+ sudoku9_transpose(Rows, Columns),
23
+ sudoku9_rows_distinct(Columns),
24
+ sudoku9_blocks(Rows),
25
+ sudoku9_flatten(Rows, Cells),
26
+ labeling([ff], Cells).
27
+
28
+ sudoku9_rows([]).
29
+ sudoku9_rows([Row|Rows]) :-
30
+ Row ins 1..9,
31
+ all_distinct(Row),
32
+ sudoku9_rows(Rows).
33
+
34
+ sudoku9_rows_distinct([]).
35
+ sudoku9_rows_distinct([Row|Rows]) :-
36
+ all_distinct(Row),
37
+ sudoku9_rows_distinct(Rows).
38
+
39
+ sudoku9_transpose([[]|_], []).
40
+ sudoku9_transpose(Rows, [Column|Columns]) :-
41
+ sudoku9_heads_tails(Rows, Column, Tails),
42
+ sudoku9_transpose(Tails, Columns).
43
+
44
+ sudoku9_heads_tails([], [], []).
45
+ sudoku9_heads_tails([[Head|Tail]|Rows], [Head|Heads], [Tail|Tails]) :-
46
+ sudoku9_heads_tails(Rows, Heads, Tails).
47
+
48
+ sudoku9_blocks([]).
49
+ sudoku9_blocks([A, B, C|Rows]) :-
50
+ sudoku9_block_row(A, B, C),
51
+ sudoku9_blocks(Rows).
52
+
53
+ sudoku9_block_row([], [], []).
54
+ sudoku9_block_row([A, B, C|As], [D, E, F|Bs], [G, H, I|Cs]) :-
55
+ all_distinct([A, B, C, D, E, F, G, H, I]),
56
+ sudoku9_block_row(As, Bs, Cs).
57
+
58
+ sudoku9_flatten([], []).
59
+ sudoku9_flatten([Row|Rows], Cells) :-
60
+ sudoku9_append(Row, Rest, Cells),
61
+ sudoku9_flatten(Rows, Rest).
62
+
63
+ sudoku9_append([], Ys, Ys).
64
+ sudoku9_append([X|Xs], Ys, [X|Zs]) :- sudoku9_append(Xs, Ys, Zs).
@@ -0,0 +1,7 @@
1
+ advanced_clpz(table, compatible(4, 0, 2)).
2
+ advanced_clpz(table, compatible(4, 3, 2)).
3
+ advanced_clpz(schedule, starts([0, 1, 3])).
4
+ advanced_clpz(cardinality, assignment([1, 1, 3], 13, >)).
5
+ advanced_clpz(cardinality, assignment([1, 3, 1], 12, >)).
6
+ advanced_clpz(cardinality, assignment([3, 1, 1], 6, <)).
7
+ advanced_clpz(circuit, successors([2, 3, 4, 1])).
@@ -0,0 +1 @@
1
+ sudoku9_solution([[1, 6, 2, 8, 5, 7, 4, 9, 3], [5, 3, 4, 1, 2, 9, 6, 7, 8], [7, 8, 9, 6, 4, 3, 5, 2, 1], [4, 7, 5, 3, 1, 2, 9, 8, 6], [9, 1, 3, 5, 8, 6, 7, 4, 2], [6, 2, 8, 7, 9, 4, 1, 3, 5], [3, 5, 6, 4, 7, 8, 2, 1, 9], [2, 4, 1, 9, 3, 5, 8, 6, 7], [8, 9, 7, 2, 6, 1, 3, 5, 4]]).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.17",
6
+ "version": "1.1.19",
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
@@ -465,8 +465,10 @@
465
465
  "chart-parser",
466
466
  "clinical-trial-screening",
467
467
  "clpz-factorial",
468
+ "clpz-global-constraints",
468
469
  "clpz-n-queens",
469
470
  "clpz-resource-allocation",
471
+ "clpz-sudoku-9x9",
470
472
  "collatz-1000",
471
473
  "combinatorics-findall-sort",
472
474
  "competitive-enzyme-kinetics",
@@ -961,7 +963,7 @@
961
963
  runButton.disabled = true;
962
964
  stopButton.disabled = false;
963
965
 
964
- const workerUrl = new URL('./src/playground-worker.js?playground=20260807b', location.href);
966
+ const workerUrl = new URL('./src/playground-worker.js?playground=20260811c', location.href);
965
967
  activeWorker = new Worker(workerUrl, { type: 'module' });
966
968
  const worker = activeWorker;
967
969
  const request = {
package/src/clpz.js CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  import { PrologError } from './iso.js';
13
13
 
14
14
  const MAX_ENUMERATED_DOMAIN = 100000;
15
+ const domainCacheByState = new WeakMap();
16
+ const mutableDomainEnvs = new WeakSet();
15
17
 
16
18
  export const clpzBuiltins = {
17
19
  register(registry) {
@@ -19,10 +21,17 @@ export const clpzBuiltins = {
19
21
  registry.add('eyeprolog__clpz_in', 2, inBuiltin, { deterministic: true, eyePrologLibrary: true });
20
22
  registry.add('eyeprolog__clpz_ins', 2, insBuiltin, { deterministic: true, eyePrologLibrary: true });
21
23
  registry.add('eyeprolog__clpz_all_distinct', 1, allDistinctBuiltin, { deterministic: true, eyePrologLibrary: true });
24
+ registry.add('eyeprolog__clpz_nvalue', 2, nvalueBuiltin, { deterministic: true, eyePrologLibrary: true });
22
25
  registry.add('eyeprolog__clpz_sum', 3, sumBuiltin, { deterministic: true, eyePrologLibrary: true });
23
26
  registry.add('eyeprolog__clpz_scalar_product', 4, scalarProductBuiltin, { deterministic: true, eyePrologLibrary: true });
27
+ registry.add('eyeprolog__clpz_tuples_in', 2, tuplesInBuiltin, { deterministic: true, eyePrologLibrary: true });
28
+ registry.add('eyeprolog__clpz_lex_chain', 1, lexChainBuiltin, { deterministic: true, eyePrologLibrary: true });
29
+ registry.add('eyeprolog__clpz_serialized', 2, serializedBuiltin, { deterministic: true, eyePrologLibrary: true });
30
+ registry.add('eyeprolog__clpz_global_cardinality', 3, globalCardinalityBuiltin, { deterministic: true, eyePrologLibrary: true });
31
+ registry.add('eyeprolog__clpz_circuit', 1, circuitBuiltin, { deterministic: true, eyePrologLibrary: true });
24
32
  registry.add('eyeprolog__clpz_chain', 2, chainBuiltin, { deterministic: true, eyePrologLibrary: true });
25
33
  registry.add('eyeprolog__clpz_element', 3, elementBuiltin, { deterministic: true, eyePrologLibrary: true });
34
+ registry.add('eyeprolog__clpz_zcompare', 3, zcompareBuiltin, { deterministic: true, eyePrologLibrary: true });
26
35
  registry.add('eyeprolog__clpz_labeling', 2, labelingBuiltin, { eyePrologLibrary: true });
27
36
  registry.add('eyeprolog__clpz_fd_var', 1, fdVarBuiltin, { deterministic: true, eyePrologLibrary: true });
28
37
  registry.add('eyeprolog__clpz_fd_inf', 2, fdBoundBuiltin('inf'), { deterministic: true, eyePrologLibrary: true });
@@ -186,40 +195,60 @@ function rootVariableName(term, env) {
186
195
  return term.type === VAR ? term.name : null;
187
196
  }
188
197
 
189
- function domainsForRoot(root, env) {
190
- const domains = [];
191
- for (const [name, values] of storeOf(env).domains) {
192
- if (rootVariableName(variable(name), env) === root) domains.push(values);
198
+ function domainsByRoot(env) {
199
+ const store = storeOf(env);
200
+ const cached = domainCacheByState.get(env._state);
201
+ if (cached?.domains === store.domains) return cached.roots;
202
+ const roots = new Map();
203
+ for (const [name, values] of store.domains) {
204
+ const resolvedRoot = rootVariableName(variable(name), env);
205
+ if (resolvedRoot == null) continue;
206
+ const current = roots.get(resolvedRoot);
207
+ roots.set(resolvedRoot, current == null ? values : intersectValues(current, values));
193
208
  }
194
- return domains;
209
+ domainCacheByState.set(env._state, { domains: store.domains, roots });
210
+ return roots;
195
211
  }
196
212
 
197
213
  function intersectValues(left, right) {
198
- const allowed = new Set(right.map(String));
199
- return left.filter((value) => allowed.has(String(value)));
214
+ const allowed = new Set(right);
215
+ return left.filter((value) => allowed.has(value));
200
216
  }
201
217
 
202
218
  function domainForRoot(root, env) {
203
- const domains = domainsForRoot(root, env);
204
- if (domains.length === 0) return null;
205
- return domains.slice(1).reduce(intersectValues, domains[0]);
219
+ return domainsByRoot(env).get(root) ?? null;
206
220
  }
207
221
 
208
222
  function constrainTermDomain(next, term, values) {
223
+ const result = narrowTermDomain(next, term, values);
224
+ return result.ok && clpzStateConsistent(next);
225
+ }
226
+
227
+ function narrowTermDomain(next, term, values) {
209
228
  const resolved = deref(term, next);
210
229
  if (resolved.type === NUMBER) {
211
230
  const value = integerValue(resolved, next);
212
- return values.some((candidate) => candidate === value);
231
+ return { ok: values.some((candidate) => candidate === value), changed: false };
213
232
  }
214
233
  if (resolved.type !== VAR) throw new PrologError('type_error(integer)', resolved);
215
234
  const existing = domainForRoot(resolved.name, next);
216
235
  const narrowed = existing == null ? values : intersectValues(existing, values);
217
- if (narrowed.length === 0) return false;
218
- const domains = new Map(storeOf(next).domains);
219
- domains.set(resolved.name, narrowed);
220
- updateStore(next, { domains });
221
- if (narrowed.length === 1 && !unify(resolved, numberTerm(narrowed[0].toString()), next)) return false;
222
- return clpzStateConsistent(next);
236
+ if (narrowed.length === 0) return { ok: false, changed: false };
237
+ const changed = existing == null || existing.length !== narrowed.length ||
238
+ existing.some((value, index) => value !== narrowed[index]);
239
+ if (!changed) return { ok: true, changed: false };
240
+ if (mutableDomainEnvs.has(next)) {
241
+ storeOf(next).domains.set(resolved.name, narrowed);
242
+ domainCacheByState.delete(next._state);
243
+ } else {
244
+ const domains = new Map(storeOf(next).domains);
245
+ domains.set(resolved.name, narrowed);
246
+ updateStore(next, { domains });
247
+ }
248
+ if (narrowed.length === 1 && !unify(resolved, numberTerm(narrowed[0].toString()), next)) {
249
+ return { ok: false, changed: false };
250
+ }
251
+ return { ok: true, changed: true };
223
252
  }
224
253
 
225
254
  function* inBuiltin({ goal, env }) {
@@ -248,11 +277,36 @@ function listArgument(term, env) {
248
277
  return items;
249
278
  }
250
279
 
280
+ function nestedListArgument(term, env) {
281
+ return listArgument(term, env).map((item) => listArgument(item, env));
282
+ }
283
+
284
+ function integerListArgument(term, env) {
285
+ return listArgument(term, env).map((item) => {
286
+ const value = integerValue(item, env);
287
+ if (value == null) throw new PrologError('instantiation_error');
288
+ return value;
289
+ });
290
+ }
291
+
292
+ function integerRange(lower, upper) {
293
+ const values = [];
294
+ for (let value = BigInt(lower); value <= BigInt(upper); value++) values.push(value);
295
+ return values;
296
+ }
297
+
251
298
  function* allDistinctBuiltin({ goal, env }) {
252
299
  const next = env.clone();
253
300
  if (addGlobal(next, { kind: 'allDistinct', terms: listArgument(goal.args[0], env) })) yield next;
254
301
  }
255
302
 
303
+ function* nvalueBuiltin({ goal, env }) {
304
+ const terms = listArgument(goal.args[1], env);
305
+ const next = env.clone();
306
+ if (!constrainTermDomain(next, goal.args[0], integerRange(terms.length === 0 ? 0 : 1, terms.length))) return;
307
+ if (addGlobal(next, { kind: 'nvalue', count: goal.args[0], terms })) yield next;
308
+ }
309
+
256
310
  function* sumBuiltin({ goal, env }) {
257
311
  const next = env.clone();
258
312
  const relation = relationName(goal.args[1], env);
@@ -269,6 +323,93 @@ function* scalarProductBuiltin({ goal, env }) {
269
323
  if (addGlobal(next, { kind: 'scalarProduct', coefficients, terms, relation, value: goal.args[3] })) yield next;
270
324
  }
271
325
 
326
+ function* tuplesInBuiltin({ goal, env }) {
327
+ const tuples = nestedListArgument(goal.args[0], env);
328
+ const relation = nestedListArgument(goal.args[1], env).map((row) => row.map((item) => {
329
+ const value = integerValue(item, env);
330
+ if (value == null) throw new PrologError('instantiation_error');
331
+ return value;
332
+ }));
333
+ const next = env.clone();
334
+ if (addGlobal(next, { kind: 'tuplesIn', tuples, relation })) yield next;
335
+ }
336
+
337
+ function* lexChainBuiltin({ goal, env }) {
338
+ const lists = nestedListArgument(goal.args[0], env);
339
+ if (lists.some((list) => list.length !== (lists[0]?.length ?? 0))) return;
340
+ const next = env.clone();
341
+ if (addGlobal(next, { kind: 'lexChain', lists })) yield next;
342
+ }
343
+
344
+ function* serializedBuiltin({ goal, env }) {
345
+ const starts = listArgument(goal.args[0], env);
346
+ const durations = integerListArgument(goal.args[1], env);
347
+ if (starts.length !== durations.length) throw new PrologError('domain_error(same_length)');
348
+ if (durations.some((duration) => duration < 0n)) throw new PrologError('domain_error(not_less_than_zero)');
349
+ const next = env.clone();
350
+ if (addGlobal(next, { kind: 'serialized', starts, durations })) yield next;
351
+ }
352
+
353
+ function gccPairs(term, env) {
354
+ const pairs = listArgument(term, env).map((pair) => {
355
+ const resolved = deref(pair, env);
356
+ if (resolved.type !== COMPOUND || resolved.name !== '-' || resolved.arity !== 2) {
357
+ throw new PrologError('domain_error(gcc_pair)', resolved);
358
+ }
359
+ const key = integerValue(resolved.args[0], env);
360
+ if (key == null) throw new PrologError('instantiation_error');
361
+ return { key, count: resolved.args[1] };
362
+ });
363
+ const keys = new Set(pairs.map(({ key }) => key.toString()));
364
+ if (keys.size !== pairs.length) throw new PrologError('domain_error(gcc_unique_key_pairs)', deref(term, env));
365
+ return pairs;
366
+ }
367
+
368
+ function gccOptions(term, env, rowCount, columnCount) {
369
+ let cost = null;
370
+ let matrix = null;
371
+ for (const option of listArgument(term, env)) {
372
+ const resolved = deref(option, env);
373
+ if (resolved.type === COMPOUND && resolved.name === 'consistency' && resolved.arity === 1 &&
374
+ deref(resolved.args[0], env).type === ATOM && deref(resolved.args[0], env).name === 'value') continue;
375
+ if (resolved.type === COMPOUND && resolved.name === 'cost' && resolved.arity === 2 && cost == null) {
376
+ cost = resolved.args[0];
377
+ matrix = nestedListArgument(resolved.args[1], env).map((row) => row.map((item) => {
378
+ const value = integerValue(item, env);
379
+ if (value == null) throw new PrologError('instantiation_error');
380
+ return value;
381
+ }));
382
+ if (matrix.length !== rowCount || matrix.some((row) => row.length !== columnCount)) {
383
+ throw new PrologError('domain_error(gcc_cost_matrix)', resolved.args[1]);
384
+ }
385
+ continue;
386
+ }
387
+ throw new PrologError('domain_error(global_cardinality_option)', resolved);
388
+ }
389
+ return { cost, matrix };
390
+ }
391
+
392
+ function* globalCardinalityBuiltin({ goal, env }) {
393
+ const terms = listArgument(goal.args[0], env);
394
+ const pairs = gccPairs(goal.args[1], env);
395
+ const options = gccOptions(goal.args[2], env, terms.length, pairs.length);
396
+ const keys = pairs.map(({ key }) => key).sort(compareBigInt);
397
+ const next = env.clone();
398
+ for (const term of terms) if (!constrainTermDomain(next, term, keys)) return;
399
+ for (const { count } of pairs) if (!constrainTermDomain(next, count, integerRange(0, terms.length))) return;
400
+ if (addGlobal(next, { kind: 'globalCardinality', terms, pairs, ...options })) yield next;
401
+ }
402
+
403
+ function* circuitBuiltin({ goal, env }) {
404
+ const terms = listArgument(goal.args[0], env);
405
+ const next = env.clone();
406
+ if (terms.length > 0) {
407
+ const values = integerRange(1, terms.length);
408
+ for (const term of terms) if (!constrainTermDomain(next, term, values)) return;
409
+ }
410
+ if (addGlobal(next, { kind: 'circuit', terms })) yield next;
411
+ }
412
+
272
413
  function* chainBuiltin({ goal, env }) {
273
414
  const terms = listArgument(goal.args[1], env);
274
415
  const relation = relationName(goal.args[0], env);
@@ -282,6 +423,70 @@ function* elementBuiltin({ goal, env }) {
282
423
  if (addGlobal(next, { kind: 'element', index: goal.args[0], terms: listArgument(goal.args[1], env), value: goal.args[2] })) yield next;
283
424
  }
284
425
 
426
+ function* zcompareBuiltin({ goal, env }) {
427
+ const next = env.clone();
428
+ if (addGlobal(next, { kind: 'zcompare', order: goal.args[0], left: goal.args[1], right: goal.args[2] })) yield next;
429
+ }
430
+
431
+ function tupleCandidates(tuple, relation, env) {
432
+ return relation.filter((row) => row.length === tuple.length && row.every((candidate, index) => {
433
+ const resolved = deref(tuple[index], env);
434
+ if (resolved.type === NUMBER) return integerValue(resolved, env) === candidate;
435
+ if (resolved.type !== VAR) throw new PrologError('type_error(integer)', resolved);
436
+ const domain = domainForRoot(resolved.name, env);
437
+ return domain == null || domain.some((value) => value === candidate);
438
+ }));
439
+ }
440
+
441
+ function lexPairTruth(left, right, env) {
442
+ if (left.length !== right.length) return false;
443
+ for (let index = 0; index < left.length; index++) {
444
+ const a = expressionValue(left[index], env);
445
+ const b = expressionValue(right[index], env);
446
+ if (a == null || b == null) return true;
447
+ if (a < b) return true;
448
+ if (a > b) return false;
449
+ }
450
+ return true;
451
+ }
452
+
453
+ function circuitTruth(terms, env) {
454
+ if (terms.length === 0) return true;
455
+ const values = terms.map((term) => expressionValue(term, env));
456
+ const seenValues = new Set();
457
+ for (let index = 0; index < values.length; index++) {
458
+ const value = values[index];
459
+ if (value == null) continue;
460
+ if (value < 1n || value > BigInt(values.length)) return false;
461
+ if (values.length > 1 && value === BigInt(index + 1)) return false;
462
+ const key = value.toString();
463
+ if (seenValues.has(key)) return false;
464
+ seenValues.add(key);
465
+ }
466
+ if (values.some((value) => value == null)) return true;
467
+ const visited = new Set();
468
+ let node = 1;
469
+ for (let step = 0; step < values.length; step++) {
470
+ if (visited.has(node)) return false;
471
+ visited.add(node);
472
+ node = Number(values[node - 1]);
473
+ }
474
+ return node === 1 && visited.size === values.length;
475
+ }
476
+
477
+ function orderAtom(term, env) {
478
+ const resolved = deref(term, env);
479
+ if (resolved.type === VAR) return null;
480
+ if (resolved.type !== ATOM || !['<', '=', '>'].includes(resolved.name)) {
481
+ throw new PrologError('domain_error(order)', resolved);
482
+ }
483
+ return resolved.name;
484
+ }
485
+
486
+ function comparedOrder(left, right) {
487
+ return left < right ? '<' : left > right ? '>' : '=';
488
+ }
489
+
285
490
  function globalTruth(global, env) {
286
491
  if (global.kind === 'allDistinct') {
287
492
  const seen = new Set();
@@ -294,6 +499,14 @@ function globalTruth(global, env) {
294
499
  }
295
500
  return true;
296
501
  }
502
+ if (global.kind === 'nvalue') {
503
+ const values = global.terms.map((term) => expressionValue(term, env));
504
+ const known = new Set(values.filter((value) => value != null).map(String));
505
+ const count = expressionValue(global.count, env);
506
+ if (count == null) return true;
507
+ if (count < BigInt(known.size) || count > BigInt(known.size + values.filter((value) => value == null).length)) return false;
508
+ return values.some((value) => value == null) || count === BigInt(known.size);
509
+ }
297
510
  if (global.kind === 'sum') {
298
511
  const values = global.terms.map((term) => expressionValue(term, env));
299
512
  const right = expressionValue(global.value, env);
@@ -309,6 +522,54 @@ function globalTruth(global, env) {
309
522
  for (let i = 0; i < values.length; i++) total += integerValue(global.coefficients[i], env) * values[i];
310
523
  return relationTruth(relationTerm(global.relation, numberTerm(total.toString()), global.value), env) !== false;
311
524
  }
525
+ if (global.kind === 'tuplesIn') {
526
+ return global.tuples.every((tuple) => tupleCandidates(tuple, global.relation, env).length > 0);
527
+ }
528
+ if (global.kind === 'lexChain') {
529
+ for (let index = 1; index < global.lists.length; index++) {
530
+ if (!lexPairTruth(global.lists[index - 1], global.lists[index], env)) return false;
531
+ }
532
+ return true;
533
+ }
534
+ if (global.kind === 'serialized') {
535
+ for (let left = 0; left < global.starts.length; left++) {
536
+ const a = expressionValue(global.starts[left], env);
537
+ if (a == null) continue;
538
+ for (let right = left + 1; right < global.starts.length; right++) {
539
+ const b = expressionValue(global.starts[right], env);
540
+ if (b == null) continue;
541
+ if (a + global.durations[left] > b && b + global.durations[right] > a) return false;
542
+ }
543
+ }
544
+ return true;
545
+ }
546
+ if (global.kind === 'globalCardinality') {
547
+ const values = global.terms.map((term) => expressionValue(term, env));
548
+ for (const { key, count: countTerm } of global.pairs) {
549
+ const known = values.filter((value) => value === key).length;
550
+ const possible = global.terms.filter((term, index) => {
551
+ if (values[index] != null) return false;
552
+ const resolved = deref(term, env);
553
+ const domain = resolved.type === VAR ? domainForRoot(resolved.name, env) : null;
554
+ return domain == null || domain.some((value) => value === key);
555
+ }).length;
556
+ const count = expressionValue(countTerm, env);
557
+ if (count != null && (count < BigInt(known) || count > BigInt(known + possible))) return false;
558
+ if (possible === 0 && count != null && count !== BigInt(known)) return false;
559
+ }
560
+ if (global.cost != null && values.every((value) => value != null)) {
561
+ let total = 0n;
562
+ for (let row = 0; row < values.length; row++) {
563
+ const column = global.pairs.findIndex(({ key }) => key === values[row]);
564
+ if (column < 0) return false;
565
+ total += global.matrix[row][column];
566
+ }
567
+ const cost = expressionValue(global.cost, env);
568
+ if (cost != null && cost !== total) return false;
569
+ }
570
+ return true;
571
+ }
572
+ if (global.kind === 'circuit') return circuitTruth(global.terms, env);
312
573
  if (global.kind === 'chain') {
313
574
  for (let i = 1; i < global.terms.length; i++) {
314
575
  const truth = relationTruth(relationTerm(global.relation, global.terms[i - 1], global.terms[i]), env);
@@ -325,6 +586,12 @@ function globalTruth(global, env) {
325
586
  const right = expressionValue(global.value, env);
326
587
  return left == null || right == null || left === right;
327
588
  }
589
+ if (global.kind === 'zcompare') {
590
+ const order = orderAtom(global.order, env);
591
+ const left = expressionValue(global.left, env);
592
+ const right = expressionValue(global.right, env);
593
+ return order == null || left == null || right == null || order === comparedOrder(left, right);
594
+ }
328
595
  return true;
329
596
  }
330
597
 
@@ -395,9 +662,143 @@ function bindLinearEquality(left, right, env) {
395
662
  return { ok: unify(variable(name), numberTerm(value.toString()), env), changed: true };
396
663
  }
397
664
 
665
+ function hasDistinctMatching(domains, forcedIndex = -1, forcedValue = null) {
666
+ const matchedByValue = new Map();
667
+ if (forcedIndex >= 0) {
668
+ if (!domains[forcedIndex].some((value) => value === forcedValue)) return false;
669
+ matchedByValue.set(forcedValue, forcedIndex);
670
+ }
671
+ const indices = domains
672
+ .map((_, index) => index)
673
+ .filter((index) => index !== forcedIndex)
674
+ .sort((left, right) => domains[left].length - domains[right].length);
675
+
676
+ function augment(index, seen) {
677
+ for (const value of domains[index]) {
678
+ if (seen.has(value)) continue;
679
+ seen.add(value);
680
+ const previous = matchedByValue.get(value);
681
+ if (previous == null ||
682
+ (previous !== forcedIndex && augment(previous, seen))) {
683
+ matchedByValue.set(value, index);
684
+ return true;
685
+ }
686
+ }
687
+ return false;
688
+ }
689
+
690
+ return indices.every((index) => augment(index, new Set()));
691
+ }
692
+
693
+ function popcount32(value) {
694
+ value >>>= 0;
695
+ value -= (value >>> 1) & 0x55555555;
696
+ value = (value & 0x33333333) + ((value >>> 2) & 0x33333333);
697
+ return (((value + (value >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
698
+ }
699
+
700
+ function hallSetDomains(domains) {
701
+ const valueIndices = new Map();
702
+ for (const domain of domains) {
703
+ for (const value of domain) {
704
+ if (!valueIndices.has(value)) valueIndices.set(value, valueIndices.size);
705
+ if (valueIndices.size > 30) return null;
706
+ }
707
+ }
708
+ const masks = domains.map((domain) => domain.reduce(
709
+ (mask, value) => mask | (1 << valueIndices.get(value)), 0));
710
+ const removed = new Array(domains.length).fill(0);
711
+ const subsetLimit = 2 ** domains.length;
712
+ for (let subset = 1; subset < subsetLimit; subset++) {
713
+ let union = 0;
714
+ for (let index = 0; index < domains.length; index++) {
715
+ if (subset & (1 << index)) union |= masks[index];
716
+ }
717
+ const variableCount = popcount32(subset);
718
+ const valueCount = popcount32(union);
719
+ if (valueCount < variableCount) return false;
720
+ if (valueCount !== variableCount) continue;
721
+ for (let index = 0; index < domains.length; index++) {
722
+ if (!(subset & (1 << index))) removed[index] |= union;
723
+ }
724
+ }
725
+ return domains.map((domain, index) => domain.filter(
726
+ (value) => !(removed[index] & (1 << valueIndices.get(value)))));
727
+ }
728
+
729
+ function propagateAllDistinct(global, env) {
730
+ const bound = new Set();
731
+ const variables = new Set();
732
+ for (const term of global.terms) {
733
+ const resolved = deref(term, env);
734
+ if (resolved.type === VAR) {
735
+ if (variables.has(resolved.name)) return { ok: false, changed: false };
736
+ variables.add(resolved.name);
737
+ continue;
738
+ }
739
+ const value = integerValue(resolved, env);
740
+ if (bound.has(value)) return { ok: false, changed: false };
741
+ bound.add(value);
742
+ }
743
+
744
+ let changed = false;
745
+ for (const term of global.terms) {
746
+ const resolved = deref(term, env);
747
+ if (resolved.type !== VAR) continue;
748
+ const domain = domainForRoot(resolved.name, env);
749
+ if (domain == null) continue;
750
+ const result = narrowTermDomain(env, term, domain.filter((value) => !bound.has(value)));
751
+ if (!result.ok) return { ok: false, changed: false };
752
+ changed ||= result.changed;
753
+ }
754
+
755
+ // Exhaustive Hall-set filtering is allocation-free in its inner loop for the
756
+ // small all_distinct/1 groups common in puzzles. Fall back to matching-based
757
+ // support when the compact bit-set representation is not applicable.
758
+ if (global.terms.length <= 16) {
759
+ const domains = global.terms.map((term) => {
760
+ const resolved = deref(term, env);
761
+ if (resolved.type === NUMBER) return [integerValue(resolved, env)];
762
+ if (resolved.type !== VAR) return null;
763
+ return domainForRoot(resolved.name, env);
764
+ });
765
+ if (domains.every((domain) => domain != null)) {
766
+ const hallDomains = global.terms.length <= 12 ? hallSetDomains(domains) : null;
767
+ if (hallDomains === false || (hallDomains == null && !hasDistinctMatching(domains))) {
768
+ return { ok: false, changed: false };
769
+ }
770
+ for (let index = 0; index < global.terms.length; index++) {
771
+ const resolved = deref(global.terms[index], env);
772
+ if (resolved.type !== VAR) continue;
773
+ const supported = hallDomains == null
774
+ ? domains[index].filter((value) => hasDistinctMatching(domains, index, value))
775
+ : hallDomains[index];
776
+ const result = narrowTermDomain(env, global.terms[index], supported);
777
+ if (!result.ok) return { ok: false, changed: false };
778
+ changed ||= result.changed;
779
+ domains[index] = supported;
780
+ }
781
+ }
782
+ }
783
+ return { ok: true, changed };
784
+ }
785
+
398
786
  function propagate(env) {
399
787
  const store = env._clpz;
400
788
  if (!store) return true;
789
+ // A propagation pass owns one domain-map copy. Individual narrowings can
790
+ // then update that private copy instead of cloning the whole map each time.
791
+ updateStore(env, { domains: new Map(store.domains) });
792
+ mutableDomainEnvs.add(env);
793
+ try {
794
+ return propagateMutable(env);
795
+ } finally {
796
+ mutableDomainEnvs.delete(env);
797
+ }
798
+ }
799
+
800
+ function propagateMutable(env) {
801
+ const store = env._clpz;
401
802
  let changed;
402
803
  do {
403
804
  changed = false;
@@ -410,7 +811,86 @@ function propagate(env) {
410
811
  }
411
812
  }
412
813
  for (const global of store.globals) {
413
- if (global.kind === 'element') {
814
+ if (global.kind === 'allDistinct') {
815
+ const result = propagateAllDistinct(global, env);
816
+ if (!result.ok) return false;
817
+ changed ||= result.changed;
818
+ } else if (global.kind === 'nvalue') {
819
+ const values = global.terms.map((term) => expressionValue(term, env));
820
+ const known = new Set(values.filter((value) => value != null).map(String));
821
+ const unresolved = values.filter((value) => value == null).length;
822
+ const result = narrowTermDomain(env, global.count, integerRange(known.size, known.size + unresolved));
823
+ if (!result.ok) return false;
824
+ changed ||= result.changed;
825
+ } else if (global.kind === 'tuplesIn') {
826
+ for (const tuple of global.tuples) {
827
+ const candidates = tupleCandidates(tuple, global.relation, env);
828
+ if (candidates.length === 0) return false;
829
+ for (let index = 0; index < tuple.length; index++) {
830
+ const values = [...new Set(candidates.map((row) => row[index].toString()))].map(BigInt).sort(compareBigInt);
831
+ const result = narrowTermDomain(env, tuple[index], values);
832
+ if (!result.ok) return false;
833
+ changed ||= result.changed;
834
+ }
835
+ }
836
+ } else if (global.kind === 'globalCardinality') {
837
+ const values = global.terms.map((term) => expressionValue(term, env));
838
+ for (const { key, count: countTerm } of global.pairs) {
839
+ const known = values.filter((value) => value === key).length;
840
+ const possibleTerms = global.terms.filter((term, index) => {
841
+ if (values[index] != null) return false;
842
+ const resolved = deref(term, env);
843
+ const domain = resolved.type === VAR ? domainForRoot(resolved.name, env) : null;
844
+ return domain == null || domain.some((value) => value === key);
845
+ });
846
+ const countResult = narrowTermDomain(env, countTerm, integerRange(known, known + possibleTerms.length));
847
+ if (!countResult.ok) return false;
848
+ changed ||= countResult.changed;
849
+ const count = expressionValue(countTerm, env);
850
+ if (count === BigInt(known)) {
851
+ for (const term of possibleTerms) {
852
+ const resolved = deref(term, env);
853
+ if (resolved.type !== VAR) continue;
854
+ const domain = domainForRoot(resolved.name, env);
855
+ const result = narrowTermDomain(env, term, domain.filter((value) => value !== key));
856
+ if (!result.ok) return false;
857
+ changed ||= result.changed;
858
+ }
859
+ } else if (count === BigInt(known + possibleTerms.length)) {
860
+ for (const term of possibleTerms) {
861
+ const result = narrowTermDomain(env, term, [key]);
862
+ if (!result.ok) return false;
863
+ changed ||= result.changed;
864
+ }
865
+ }
866
+ }
867
+ if (global.cost != null && values.every((value) => value != null)) {
868
+ let total = 0n;
869
+ for (let row = 0; row < values.length; row++) {
870
+ const column = global.pairs.findIndex(({ key }) => key === values[row]);
871
+ if (column < 0) return false;
872
+ total += global.matrix[row][column];
873
+ }
874
+ const result = bindExpressionEquality(global.cost, numberTerm(total.toString()), env);
875
+ if (!result.ok) return false;
876
+ changed ||= result.changed;
877
+ }
878
+ } else if (global.kind === 'zcompare') {
879
+ const order = orderAtom(global.order, env);
880
+ const left = expressionValue(global.left, env);
881
+ const right = expressionValue(global.right, env);
882
+ if (left != null && right != null) {
883
+ const resolvedOrder = atom(comparedOrder(left, right));
884
+ if (order == null) {
885
+ if (!unify(global.order, resolvedOrder, env)) return false;
886
+ changed = true;
887
+ } else if (order !== resolvedOrder.name) return false;
888
+ } else if (order === '=') {
889
+ const result = bindLinearEquality(global.left, global.right, env);
890
+ if (!result.ok) return false;
891
+ changed ||= result.changed;
892
+ }
893
+ } else if (global.kind === 'element') {
414
894
  const index = expressionValue(global.index, env);
415
895
  if (index != null) {
416
896
  if (index < 1n || index > BigInt(global.terms.length)) return false;
package/src/lib/clpz.pl CHANGED
@@ -25,13 +25,21 @@
25
25
  ins/2,
26
26
  all_different/1,
27
27
  all_distinct/1,
28
+ nvalue/2,
28
29
  sum/3,
29
30
  scalar_product/4,
31
+ tuples_in/2,
30
32
  labeling/2,
31
33
  label/1,
32
34
  indomain/1,
35
+ lex_chain/1,
36
+ serialized/2,
37
+ global_cardinality/2,
38
+ global_cardinality/3,
39
+ circuit/1,
33
40
  chain/2,
34
41
  element/3,
42
+ zcompare/3,
35
43
  fd_var/1,
36
44
  fd_inf/2,
37
45
  fd_sup/2,
@@ -75,6 +83,7 @@ ins(Integers, Domain) :- eyeprolog__clpz_ins(Integers, Domain).
75
83
 
76
84
  all_different(Integers) :- all_distinct(Integers).
77
85
  all_distinct(Integers) :- eyeprolog__clpz_all_distinct(Integers).
86
+ nvalue(Count, Integers) :- eyeprolog__clpz_nvalue(Count, Integers).
78
87
 
79
88
  sum(Integers, Relation, Value) :-
80
89
  eyeprolog__clpz_sum(Integers, Relation, Value).
@@ -82,15 +91,27 @@ sum(Integers, Relation, Value) :-
82
91
  scalar_product(Coefficients, Integers, Relation, Value) :-
83
92
  eyeprolog__clpz_scalar_product(Coefficients, Integers, Relation, Value).
84
93
 
94
+ tuples_in(Tuples, Relation) :- eyeprolog__clpz_tuples_in(Tuples, Relation).
95
+
85
96
  labeling(Options, Integers) :-
86
97
  eyeprolog__clpz_labeling(Options, Integers).
87
98
 
88
99
  label(Integers) :- labeling([], Integers).
89
100
  indomain(Integer) :- labeling([], [Integer]).
90
101
 
102
+ lex_chain(Lists) :- eyeprolog__clpz_lex_chain(Lists).
103
+ serialized(Starts, Durations) :- eyeprolog__clpz_serialized(Starts, Durations).
104
+
105
+ global_cardinality(Integers, Pairs) :-
106
+ global_cardinality(Integers, Pairs, []).
107
+ global_cardinality(Integers, Pairs, Options) :-
108
+ eyeprolog__clpz_global_cardinality(Integers, Pairs, Options).
109
+
110
+ circuit(Integers) :- eyeprolog__clpz_circuit(Integers).
91
111
  chain(Relation, Integers) :- eyeprolog__clpz_chain(Relation, Integers).
92
112
  element(Index, Integers, Value) :-
93
113
  eyeprolog__clpz_element(Index, Integers, Value).
114
+ zcompare(Order, Left, Right) :- eyeprolog__clpz_zcompare(Order, Left, Right).
94
115
 
95
116
  fd_var(Integer) :- eyeprolog__clpz_fd_var(Integer).
96
117
  fd_inf(Integer, Infimum) :- eyeprolog__clpz_fd_inf(Integer, Infimum).
@@ -1,7 +1,7 @@
1
1
  // Browser worker entry used by playground.html.
2
2
  // Keep this module free of Node-only imports: it is fetched directly by the
3
3
  // browser and is also exercised by test/run-playground.mjs.
4
- import { createEyePrologRegistry, run } from './index.js?playground=20260807b';
4
+ import { createEyePrologRegistry, run } from './index.js?playground=20260811c';
5
5
 
6
6
  const registry = createEyePrologRegistry();
7
7
 
@@ -22,7 +22,7 @@ const moduleFiles = Object.freeze({
22
22
 
23
23
  const cacheKey = isNode
24
24
  ? null
25
- : (new URL(import.meta.url).searchParams.get('playground') ?? '20260811b');
25
+ : (new URL(import.meta.url).searchParams.get('playground') ?? '20260811c');
26
26
 
27
27
  export const standardLibrarySources = new Map(await Promise.all(
28
28
  Object.entries(moduleFiles).map(async ([name, filename]) => [name, {
@@ -48,9 +48,11 @@ export const eyePrologNativeLibraryIndicators = Object.freeze([
48
48
  'call_nth/2', 'freeze/2',
49
49
  '#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
50
50
  '#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
51
- 'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'sum/3',
52
- 'scalar_product/4', 'labeling/2', 'label/1', 'indomain/1', 'chain/2',
53
- 'element/3', 'fd_var/1', 'fd_inf/2', 'fd_sup/2', 'fd_size/2', 'fd_dom/2',
51
+ 'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
52
+ 'scalar_product/4', 'tuples_in/2', 'labeling/2', 'label/1',
53
+ 'indomain/1', 'lex_chain/1', 'serialized/2', 'global_cardinality/2',
54
+ 'global_cardinality/3', 'circuit/1', 'chain/2', 'element/3', 'zcompare/3',
55
+ 'fd_var/1', 'fd_inf/2', 'fd_sup/2', 'fd_size/2', 'fd_dom/2',
54
56
  ]);
55
57
  export const eyePrologPortableLibraryIndicators = Object.freeze([
56
58
  'sumall/3', 'aggregate_min/5', 'aggregate_max/5',
@@ -1438,20 +1438,21 @@ open(X) :- candidate(X), \\+ closed(X).
1438
1438
  assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
1439
1439
  assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
1440
1440
  assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
1441
- assertEqual(library.defs.size, 145, 'EyeProlog registry contains ISO definitions and private library adapters');
1441
+ assertEqual(library.defs.size, 152, 'EyeProlog registry contains ISO definitions and private library adapters');
1442
1442
  assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
1443
1443
  assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
1444
- assertEqual(registeredNativeEyePrologLibraryNames().length, 31, 'public native EyeProlog builtin count');
1444
+ assertEqual(registeredNativeEyePrologLibraryNames().length, 39, 'public native EyeProlog builtin count');
1445
1445
  assertEqual(eyePrologPortableLibraryIndicators.length, 60, 'portable Prolog library count');
1446
- assertEqual(eyePrologNativeLibraryIndicators.length, 31, 'native host library count');
1446
+ assertEqual(eyePrologNativeLibraryIndicators.length, 39, 'native host library count');
1447
1447
  assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
1448
- assertEqual(eyePrologLibraryIndicators.length, 91, 'complete EyeProlog library surface');
1448
+ assertEqual(eyePrologLibraryIndicators.length, 99, 'complete EyeProlog library surface');
1449
1449
  assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
1450
1450
  assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
1451
1451
  assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
1452
1452
  assertEqual(registry.get('eyeprolog__freeze', 2), null, 'private freeze adapter is absent from ISO registry');
1453
1453
  assertEqual(Boolean(library.get('eyeprolog__freeze', 2)), true, 'private freeze adapter is registered for EyeProlog');
1454
1454
  assertEqual(Boolean(library.get('eyeprolog__clpz_labeling', 2)), true, 'private CLP(Z) labeling adapter is registered');
1455
+ assertEqual(Boolean(library.get('eyeprolog__clpz_global_cardinality', 3)), true, 'private CLP(Z) cardinality adapter is registered');
1455
1456
  assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
1456
1457
  assertEqual(library.get('smallest_divisor_from', 3), null, 'smallest_divisor_from/3 remains portable Prolog');
1457
1458
  assertEqual(library.get('random', 3), null, 'random/3 remains portable Prolog');
@@ -1539,10 +1540,26 @@ answer(X, Y, B) :-
1539
1540
  B #<==> X #= 1,
1540
1541
  labeling([ff, down], [X, Y, B]).
1541
1542
  contradiction :- Z in 1..3, Z = 4.
1543
+ pruned(Domain) :- [X, Y] ins 1..3, all_distinct([X, Y]), X #= 2, fd_dom(Y, Domain).
1544
+ hall(Domain) :- [X, Y] ins 1..2, Z in 1..3, all_distinct([X, Y, Z]), fd_dom(Z, Domain).
1545
+ repeated(X) :- X in 1..3, all_distinct([X, X]).
1542
1546
  `);
1543
1547
  assertEqual(program.findGroup('labeling', 2)?.module, 'clpz', 'labeling/2 module');
1544
- assertEqual(run(program, { goals: ['answer(X, Y, B)', 'contradiction'] }).stdout,
1545
- 'answer(1, 4, 1).\nanswer(2, 3, 0).\n', 'CLP(Z) constrained answers');
1548
+ assertEqual(run(program, { goals: ['answer(X, Y, B)', 'contradiction', 'pruned(Domain)', 'hall(Domain)', 'repeated(X)'] }).stdout,
1549
+ 'answer(1, 4, 1).\nanswer(2, 3, 0).\npruned(1 \\/ 3).\nhall(3).\n', 'CLP(Z) constrained answers');
1550
+ },
1551
+ },
1552
+ {
1553
+ name: 'CLP(Z) finite global constraints match the portable example',
1554
+ run: () => {
1555
+ const filename = path.join(packageRoot, 'examples', 'clpz-global-constraints.pl');
1556
+ const source = fs.readFileSync(filename, 'utf8');
1557
+ const program = Program.parseSources([{ text: source, filename }]);
1558
+ assertEqual(program.findGroup('tuples_in', 2)?.module, 'clpz', 'tuples_in/2 module');
1559
+ assertEqual(program.findGroup('global_cardinality', 3)?.module, 'clpz', 'global_cardinality/3 module');
1560
+ assertEqual(program.findGroup('circuit', 1)?.module, 'clpz', 'circuit/1 module');
1561
+ const expected = fs.readFileSync(path.join(packageRoot, 'examples', 'output', 'clpz-global-constraints.pl'), 'utf8');
1562
+ assertEqual(run(program, { goal: 'advanced_clpz(X0, X1)' }).stdout, expected, 'global constraint answers');
1546
1563
  },
1547
1564
  },
1548
1565
  {
@@ -5762,14 +5762,14 @@ so side effects occur in Prolog execution order.
5762
5762
 
5763
5763
  ### The EyeProlog library
5764
5764
 
5765
- EyeProlog exposes **91 library predicate indicators** in addition to the 129
5765
+ EyeProlog exposes **99 library predicate indicators** in addition to the 129
5766
5766
  indicators in its isolated ISO profile. **60 are implemented entirely as
5767
- ordinary Prolog clauses** in focused modules under `src/lib/`. The remaining 31
5767
+ ordinary Prolog clauses** in focused modules under `src/lib/`. The remaining 39
5768
5768
  are public wrappers around backtrackable host support: Prologue `call_nth/2` and
5769
- `freeze/2`, plus the 29-predicate finite-domain `library(clpz)` kernel. The
5770
- resulting normal EyeProlog language surface is therefore **220 public predicate
5769
+ `freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
5770
+ resulting normal EyeProlog language surface is therefore **228 public predicate
5771
5771
  indicators**. Internally, the runtime registry contains the 129 ISO definitions
5772
- plus 16 private library adapters; public relations remain module source clauses.
5772
+ plus 23 private library adapters; public relations remain module source clauses.
5773
5773
 
5774
5774
  The sources are `src/lib/aggregate.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
5775
5775
  `src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lists.pl`,
@@ -5799,7 +5799,7 @@ between solution branches.
5799
5799
  | Module | Exported predicate indicators |
5800
5800
  | --- | --- |
5801
5801
  | `library(aggregate)` | `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
5802
- | `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `sum/3`, `scalar_product/4`, `labeling/2`, `label/1`, `indomain/1`, `chain/2`, `element/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
5802
+ | `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `chain/2`, `element/3`, `zcompare/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
5803
5803
  | `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
5804
5804
  | `library(dates)` | `difference/3` |
5805
5805
  | `library(iso_ext)` | `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
@@ -5816,10 +5816,13 @@ between solution branches.
5816
5816
  programming over integers. Its first implementation step provides finite
5817
5817
  interval and union domains, arithmetic and reified constraints, backtrackable
5818
5818
  labeling with `ff`, `up`, and `down`, global distinctness, linear sums and
5819
- scalar products, chains, elements, and domain reflection. Constraints are kept
5820
- in the logical environment, so failed alternatives cannot leak domains into
5821
- later branches. The full Trealla library also contains additional global
5822
- constraints and unbounded-domain propagation that are not yet exported here.
5819
+ scalar products, chains, elements, value counting, extensional tuple tables,
5820
+ lexicographic chains, serialized schedules, global cardinality with costs,
5821
+ Hamiltonian circuits, three-way comparison, and domain reflection. Constraints
5822
+ are kept in the logical environment, so failed alternatives cannot leak domains
5823
+ into later branches. The full Trealla library also contains automata,
5824
+ cumulative and two-dimensional scheduling constraints, and unbounded-domain
5825
+ propagation that are not yet exported here.
5823
5826
 
5824
5827
  `library(iso_ext)` collects extensions commonly found in mature Prolog
5825
5828
  systems. `forall/2` checks an action for every solution of a condition;
@@ -6362,7 +6365,7 @@ Review questions:
6362
6365
  </figure>
6363
6366
 
6364
6367
  The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
6365
- top-level directory contains **209 self-contained runnable programs**. Every
6368
+ top-level directory contains **211 self-contained runnable programs**. Every
6366
6369
  source program has an exact answer file under
6367
6370
  [examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
6368
6371
  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
@@ -6411,8 +6414,10 @@ mode at a time.
6411
6414
  | Program | Standard facility | Checked answer |
6412
6415
  | --- | --- | --- |
6413
6416
  | [CLP(Z) factorial](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-factorial.pl) | Declarative predecessor and product constraints propagate a factorial without mode-sensitive `is/2`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-factorial.pl) |
6417
+ | [CLP(Z) global constraints](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-global-constraints.pl) | Compatibility tables, lexicographic and serialized schedules, global cardinality with costs, circuits, value counting, and integer comparison. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-global-constraints.pl) |
6414
6418
  | [CLP(Z) N-queens](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-n-queens.pl) | Six-queens using finite domains, delayed diagonal constraints, `all_distinct/1`, and first-fail labeling. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-n-queens.pl) |
6415
6419
  | [CLP(Z) resource allocation](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-resource-allocation.pl) | Resource assignment using `element/3`, `sum/3`, `scalar_product/4`, reification, labeling options, and domain reflection. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-resource-allocation.pl) |
6420
+ | [CLP(Z) Sudoku 9×9](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-sudoku-9x9.pl) | The difficult AI Escargot puzzle modeled with finite domains, 27 all-distinct constraints, and first-fail labeling. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-sudoku-9x9.pl) |
6416
6421
  | [Combinatorics Findall Sort](https://github.com/eyereasoner/eyeprolog/blob/main/examples/combinatorics-findall-sort.pl) | Eyelet-inspired combinations example using `findall/3` and ISO `sort/2`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/combinatorics-findall-sort.pl) |
6417
6422
  | [Floating Point](https://github.com/eyereasoner/eyeprolog/blob/main/examples/floating-point.pl) | Floating-point arithmetic and comparisons. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/floating-point.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/floating-point.pl) |
6418
6423
  | [Atomic conversion](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-atomic-conversion.pl) | Atom splitting, character atoms, Unicode codes, and numeric parsing. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-atomic-conversion.pl) |