eyeprolog 1.1.17 → 1.1.18

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,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])).
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.18",
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,6 +465,7 @@
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",
470
471
  "collatz-1000",
@@ -961,7 +962,7 @@
961
962
  runButton.disabled = true;
962
963
  stopButton.disabled = false;
963
964
 
964
- const workerUrl = new URL('./src/playground-worker.js?playground=20260807b', location.href);
965
+ const workerUrl = new URL('./src/playground-worker.js?playground=20260811c', location.href);
965
966
  activeWorker = new Worker(workerUrl, { type: 'module' });
966
967
  const worker = activeWorker;
967
968
  const request = {
package/src/clpz.js CHANGED
@@ -19,10 +19,17 @@ export const clpzBuiltins = {
19
19
  registry.add('eyeprolog__clpz_in', 2, inBuiltin, { deterministic: true, eyePrologLibrary: true });
20
20
  registry.add('eyeprolog__clpz_ins', 2, insBuiltin, { deterministic: true, eyePrologLibrary: true });
21
21
  registry.add('eyeprolog__clpz_all_distinct', 1, allDistinctBuiltin, { deterministic: true, eyePrologLibrary: true });
22
+ registry.add('eyeprolog__clpz_nvalue', 2, nvalueBuiltin, { deterministic: true, eyePrologLibrary: true });
22
23
  registry.add('eyeprolog__clpz_sum', 3, sumBuiltin, { deterministic: true, eyePrologLibrary: true });
23
24
  registry.add('eyeprolog__clpz_scalar_product', 4, scalarProductBuiltin, { deterministic: true, eyePrologLibrary: true });
25
+ registry.add('eyeprolog__clpz_tuples_in', 2, tuplesInBuiltin, { deterministic: true, eyePrologLibrary: true });
26
+ registry.add('eyeprolog__clpz_lex_chain', 1, lexChainBuiltin, { deterministic: true, eyePrologLibrary: true });
27
+ registry.add('eyeprolog__clpz_serialized', 2, serializedBuiltin, { deterministic: true, eyePrologLibrary: true });
28
+ registry.add('eyeprolog__clpz_global_cardinality', 3, globalCardinalityBuiltin, { deterministic: true, eyePrologLibrary: true });
29
+ registry.add('eyeprolog__clpz_circuit', 1, circuitBuiltin, { deterministic: true, eyePrologLibrary: true });
24
30
  registry.add('eyeprolog__clpz_chain', 2, chainBuiltin, { deterministic: true, eyePrologLibrary: true });
25
31
  registry.add('eyeprolog__clpz_element', 3, elementBuiltin, { deterministic: true, eyePrologLibrary: true });
32
+ registry.add('eyeprolog__clpz_zcompare', 3, zcompareBuiltin, { deterministic: true, eyePrologLibrary: true });
26
33
  registry.add('eyeprolog__clpz_labeling', 2, labelingBuiltin, { eyePrologLibrary: true });
27
34
  registry.add('eyeprolog__clpz_fd_var', 1, fdVarBuiltin, { deterministic: true, eyePrologLibrary: true });
28
35
  registry.add('eyeprolog__clpz_fd_inf', 2, fdBoundBuiltin('inf'), { deterministic: true, eyePrologLibrary: true });
@@ -206,20 +213,30 @@ function domainForRoot(root, env) {
206
213
  }
207
214
 
208
215
  function constrainTermDomain(next, term, values) {
216
+ const result = narrowTermDomain(next, term, values);
217
+ return result.ok && clpzStateConsistent(next);
218
+ }
219
+
220
+ function narrowTermDomain(next, term, values) {
209
221
  const resolved = deref(term, next);
210
222
  if (resolved.type === NUMBER) {
211
223
  const value = integerValue(resolved, next);
212
- return values.some((candidate) => candidate === value);
224
+ return { ok: values.some((candidate) => candidate === value), changed: false };
213
225
  }
214
226
  if (resolved.type !== VAR) throw new PrologError('type_error(integer)', resolved);
215
227
  const existing = domainForRoot(resolved.name, next);
216
228
  const narrowed = existing == null ? values : intersectValues(existing, values);
217
- if (narrowed.length === 0) return false;
229
+ if (narrowed.length === 0) return { ok: false, changed: false };
230
+ const changed = existing == null || existing.length !== narrowed.length ||
231
+ existing.some((value, index) => value !== narrowed[index]);
232
+ if (!changed) return { ok: true, changed: false };
218
233
  const domains = new Map(storeOf(next).domains);
219
234
  domains.set(resolved.name, narrowed);
220
235
  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 === 1 && !unify(resolved, numberTerm(narrowed[0].toString()), next)) {
237
+ return { ok: false, changed: false };
238
+ }
239
+ return { ok: true, changed: true };
223
240
  }
224
241
 
225
242
  function* inBuiltin({ goal, env }) {
@@ -248,11 +265,36 @@ function listArgument(term, env) {
248
265
  return items;
249
266
  }
250
267
 
268
+ function nestedListArgument(term, env) {
269
+ return listArgument(term, env).map((item) => listArgument(item, env));
270
+ }
271
+
272
+ function integerListArgument(term, env) {
273
+ return listArgument(term, env).map((item) => {
274
+ const value = integerValue(item, env);
275
+ if (value == null) throw new PrologError('instantiation_error');
276
+ return value;
277
+ });
278
+ }
279
+
280
+ function integerRange(lower, upper) {
281
+ const values = [];
282
+ for (let value = BigInt(lower); value <= BigInt(upper); value++) values.push(value);
283
+ return values;
284
+ }
285
+
251
286
  function* allDistinctBuiltin({ goal, env }) {
252
287
  const next = env.clone();
253
288
  if (addGlobal(next, { kind: 'allDistinct', terms: listArgument(goal.args[0], env) })) yield next;
254
289
  }
255
290
 
291
+ function* nvalueBuiltin({ goal, env }) {
292
+ const terms = listArgument(goal.args[1], env);
293
+ const next = env.clone();
294
+ if (!constrainTermDomain(next, goal.args[0], integerRange(terms.length === 0 ? 0 : 1, terms.length))) return;
295
+ if (addGlobal(next, { kind: 'nvalue', count: goal.args[0], terms })) yield next;
296
+ }
297
+
256
298
  function* sumBuiltin({ goal, env }) {
257
299
  const next = env.clone();
258
300
  const relation = relationName(goal.args[1], env);
@@ -269,6 +311,93 @@ function* scalarProductBuiltin({ goal, env }) {
269
311
  if (addGlobal(next, { kind: 'scalarProduct', coefficients, terms, relation, value: goal.args[3] })) yield next;
270
312
  }
271
313
 
314
+ function* tuplesInBuiltin({ goal, env }) {
315
+ const tuples = nestedListArgument(goal.args[0], env);
316
+ const relation = nestedListArgument(goal.args[1], env).map((row) => row.map((item) => {
317
+ const value = integerValue(item, env);
318
+ if (value == null) throw new PrologError('instantiation_error');
319
+ return value;
320
+ }));
321
+ const next = env.clone();
322
+ if (addGlobal(next, { kind: 'tuplesIn', tuples, relation })) yield next;
323
+ }
324
+
325
+ function* lexChainBuiltin({ goal, env }) {
326
+ const lists = nestedListArgument(goal.args[0], env);
327
+ if (lists.some((list) => list.length !== (lists[0]?.length ?? 0))) return;
328
+ const next = env.clone();
329
+ if (addGlobal(next, { kind: 'lexChain', lists })) yield next;
330
+ }
331
+
332
+ function* serializedBuiltin({ goal, env }) {
333
+ const starts = listArgument(goal.args[0], env);
334
+ const durations = integerListArgument(goal.args[1], env);
335
+ if (starts.length !== durations.length) throw new PrologError('domain_error(same_length)');
336
+ if (durations.some((duration) => duration < 0n)) throw new PrologError('domain_error(not_less_than_zero)');
337
+ const next = env.clone();
338
+ if (addGlobal(next, { kind: 'serialized', starts, durations })) yield next;
339
+ }
340
+
341
+ function gccPairs(term, env) {
342
+ const pairs = listArgument(term, env).map((pair) => {
343
+ const resolved = deref(pair, env);
344
+ if (resolved.type !== COMPOUND || resolved.name !== '-' || resolved.arity !== 2) {
345
+ throw new PrologError('domain_error(gcc_pair)', resolved);
346
+ }
347
+ const key = integerValue(resolved.args[0], env);
348
+ if (key == null) throw new PrologError('instantiation_error');
349
+ return { key, count: resolved.args[1] };
350
+ });
351
+ const keys = new Set(pairs.map(({ key }) => key.toString()));
352
+ if (keys.size !== pairs.length) throw new PrologError('domain_error(gcc_unique_key_pairs)', deref(term, env));
353
+ return pairs;
354
+ }
355
+
356
+ function gccOptions(term, env, rowCount, columnCount) {
357
+ let cost = null;
358
+ let matrix = null;
359
+ for (const option of listArgument(term, env)) {
360
+ const resolved = deref(option, env);
361
+ if (resolved.type === COMPOUND && resolved.name === 'consistency' && resolved.arity === 1 &&
362
+ deref(resolved.args[0], env).type === ATOM && deref(resolved.args[0], env).name === 'value') continue;
363
+ if (resolved.type === COMPOUND && resolved.name === 'cost' && resolved.arity === 2 && cost == null) {
364
+ cost = resolved.args[0];
365
+ matrix = nestedListArgument(resolved.args[1], env).map((row) => row.map((item) => {
366
+ const value = integerValue(item, env);
367
+ if (value == null) throw new PrologError('instantiation_error');
368
+ return value;
369
+ }));
370
+ if (matrix.length !== rowCount || matrix.some((row) => row.length !== columnCount)) {
371
+ throw new PrologError('domain_error(gcc_cost_matrix)', resolved.args[1]);
372
+ }
373
+ continue;
374
+ }
375
+ throw new PrologError('domain_error(global_cardinality_option)', resolved);
376
+ }
377
+ return { cost, matrix };
378
+ }
379
+
380
+ function* globalCardinalityBuiltin({ goal, env }) {
381
+ const terms = listArgument(goal.args[0], env);
382
+ const pairs = gccPairs(goal.args[1], env);
383
+ const options = gccOptions(goal.args[2], env, terms.length, pairs.length);
384
+ const keys = pairs.map(({ key }) => key).sort(compareBigInt);
385
+ const next = env.clone();
386
+ for (const term of terms) if (!constrainTermDomain(next, term, keys)) return;
387
+ for (const { count } of pairs) if (!constrainTermDomain(next, count, integerRange(0, terms.length))) return;
388
+ if (addGlobal(next, { kind: 'globalCardinality', terms, pairs, ...options })) yield next;
389
+ }
390
+
391
+ function* circuitBuiltin({ goal, env }) {
392
+ const terms = listArgument(goal.args[0], env);
393
+ const next = env.clone();
394
+ if (terms.length > 0) {
395
+ const values = integerRange(1, terms.length);
396
+ for (const term of terms) if (!constrainTermDomain(next, term, values)) return;
397
+ }
398
+ if (addGlobal(next, { kind: 'circuit', terms })) yield next;
399
+ }
400
+
272
401
  function* chainBuiltin({ goal, env }) {
273
402
  const terms = listArgument(goal.args[1], env);
274
403
  const relation = relationName(goal.args[0], env);
@@ -282,6 +411,70 @@ function* elementBuiltin({ goal, env }) {
282
411
  if (addGlobal(next, { kind: 'element', index: goal.args[0], terms: listArgument(goal.args[1], env), value: goal.args[2] })) yield next;
283
412
  }
284
413
 
414
+ function* zcompareBuiltin({ goal, env }) {
415
+ const next = env.clone();
416
+ if (addGlobal(next, { kind: 'zcompare', order: goal.args[0], left: goal.args[1], right: goal.args[2] })) yield next;
417
+ }
418
+
419
+ function tupleCandidates(tuple, relation, env) {
420
+ return relation.filter((row) => row.length === tuple.length && row.every((candidate, index) => {
421
+ const resolved = deref(tuple[index], env);
422
+ if (resolved.type === NUMBER) return integerValue(resolved, env) === candidate;
423
+ if (resolved.type !== VAR) throw new PrologError('type_error(integer)', resolved);
424
+ const domain = domainForRoot(resolved.name, env);
425
+ return domain == null || domain.some((value) => value === candidate);
426
+ }));
427
+ }
428
+
429
+ function lexPairTruth(left, right, env) {
430
+ if (left.length !== right.length) return false;
431
+ for (let index = 0; index < left.length; index++) {
432
+ const a = expressionValue(left[index], env);
433
+ const b = expressionValue(right[index], env);
434
+ if (a == null || b == null) return true;
435
+ if (a < b) return true;
436
+ if (a > b) return false;
437
+ }
438
+ return true;
439
+ }
440
+
441
+ function circuitTruth(terms, env) {
442
+ if (terms.length === 0) return true;
443
+ const values = terms.map((term) => expressionValue(term, env));
444
+ const seenValues = new Set();
445
+ for (let index = 0; index < values.length; index++) {
446
+ const value = values[index];
447
+ if (value == null) continue;
448
+ if (value < 1n || value > BigInt(values.length)) return false;
449
+ if (values.length > 1 && value === BigInt(index + 1)) return false;
450
+ const key = value.toString();
451
+ if (seenValues.has(key)) return false;
452
+ seenValues.add(key);
453
+ }
454
+ if (values.some((value) => value == null)) return true;
455
+ const visited = new Set();
456
+ let node = 1;
457
+ for (let step = 0; step < values.length; step++) {
458
+ if (visited.has(node)) return false;
459
+ visited.add(node);
460
+ node = Number(values[node - 1]);
461
+ }
462
+ return node === 1 && visited.size === values.length;
463
+ }
464
+
465
+ function orderAtom(term, env) {
466
+ const resolved = deref(term, env);
467
+ if (resolved.type === VAR) return null;
468
+ if (resolved.type !== ATOM || !['<', '=', '>'].includes(resolved.name)) {
469
+ throw new PrologError('domain_error(order)', resolved);
470
+ }
471
+ return resolved.name;
472
+ }
473
+
474
+ function comparedOrder(left, right) {
475
+ return left < right ? '<' : left > right ? '>' : '=';
476
+ }
477
+
285
478
  function globalTruth(global, env) {
286
479
  if (global.kind === 'allDistinct') {
287
480
  const seen = new Set();
@@ -294,6 +487,14 @@ function globalTruth(global, env) {
294
487
  }
295
488
  return true;
296
489
  }
490
+ if (global.kind === 'nvalue') {
491
+ const values = global.terms.map((term) => expressionValue(term, env));
492
+ const known = new Set(values.filter((value) => value != null).map(String));
493
+ const count = expressionValue(global.count, env);
494
+ if (count == null) return true;
495
+ if (count < BigInt(known.size) || count > BigInt(known.size + values.filter((value) => value == null).length)) return false;
496
+ return values.some((value) => value == null) || count === BigInt(known.size);
497
+ }
297
498
  if (global.kind === 'sum') {
298
499
  const values = global.terms.map((term) => expressionValue(term, env));
299
500
  const right = expressionValue(global.value, env);
@@ -309,6 +510,54 @@ function globalTruth(global, env) {
309
510
  for (let i = 0; i < values.length; i++) total += integerValue(global.coefficients[i], env) * values[i];
310
511
  return relationTruth(relationTerm(global.relation, numberTerm(total.toString()), global.value), env) !== false;
311
512
  }
513
+ if (global.kind === 'tuplesIn') {
514
+ return global.tuples.every((tuple) => tupleCandidates(tuple, global.relation, env).length > 0);
515
+ }
516
+ if (global.kind === 'lexChain') {
517
+ for (let index = 1; index < global.lists.length; index++) {
518
+ if (!lexPairTruth(global.lists[index - 1], global.lists[index], env)) return false;
519
+ }
520
+ return true;
521
+ }
522
+ if (global.kind === 'serialized') {
523
+ for (let left = 0; left < global.starts.length; left++) {
524
+ const a = expressionValue(global.starts[left], env);
525
+ if (a == null) continue;
526
+ for (let right = left + 1; right < global.starts.length; right++) {
527
+ const b = expressionValue(global.starts[right], env);
528
+ if (b == null) continue;
529
+ if (a + global.durations[left] > b && b + global.durations[right] > a) return false;
530
+ }
531
+ }
532
+ return true;
533
+ }
534
+ if (global.kind === 'globalCardinality') {
535
+ const values = global.terms.map((term) => expressionValue(term, env));
536
+ for (const { key, count: countTerm } of global.pairs) {
537
+ const known = values.filter((value) => value === key).length;
538
+ const possible = global.terms.filter((term, index) => {
539
+ if (values[index] != null) return false;
540
+ const resolved = deref(term, env);
541
+ const domain = resolved.type === VAR ? domainForRoot(resolved.name, env) : null;
542
+ return domain == null || domain.some((value) => value === key);
543
+ }).length;
544
+ const count = expressionValue(countTerm, env);
545
+ if (count != null && (count < BigInt(known) || count > BigInt(known + possible))) return false;
546
+ if (possible === 0 && count != null && count !== BigInt(known)) return false;
547
+ }
548
+ if (global.cost != null && values.every((value) => value != null)) {
549
+ let total = 0n;
550
+ for (let row = 0; row < values.length; row++) {
551
+ const column = global.pairs.findIndex(({ key }) => key === values[row]);
552
+ if (column < 0) return false;
553
+ total += global.matrix[row][column];
554
+ }
555
+ const cost = expressionValue(global.cost, env);
556
+ if (cost != null && cost !== total) return false;
557
+ }
558
+ return true;
559
+ }
560
+ if (global.kind === 'circuit') return circuitTruth(global.terms, env);
312
561
  if (global.kind === 'chain') {
313
562
  for (let i = 1; i < global.terms.length; i++) {
314
563
  const truth = relationTruth(relationTerm(global.relation, global.terms[i - 1], global.terms[i]), env);
@@ -325,6 +574,12 @@ function globalTruth(global, env) {
325
574
  const right = expressionValue(global.value, env);
326
575
  return left == null || right == null || left === right;
327
576
  }
577
+ if (global.kind === 'zcompare') {
578
+ const order = orderAtom(global.order, env);
579
+ const left = expressionValue(global.left, env);
580
+ const right = expressionValue(global.right, env);
581
+ return order == null || left == null || right == null || order === comparedOrder(left, right);
582
+ }
328
583
  return true;
329
584
  }
330
585
 
@@ -410,7 +665,82 @@ function propagate(env) {
410
665
  }
411
666
  }
412
667
  for (const global of store.globals) {
413
- if (global.kind === 'element') {
668
+ if (global.kind === 'nvalue') {
669
+ const values = global.terms.map((term) => expressionValue(term, env));
670
+ const known = new Set(values.filter((value) => value != null).map(String));
671
+ const unresolved = values.filter((value) => value == null).length;
672
+ const result = narrowTermDomain(env, global.count, integerRange(known.size, known.size + unresolved));
673
+ if (!result.ok) return false;
674
+ changed ||= result.changed;
675
+ } else if (global.kind === 'tuplesIn') {
676
+ for (const tuple of global.tuples) {
677
+ const candidates = tupleCandidates(tuple, global.relation, env);
678
+ if (candidates.length === 0) return false;
679
+ for (let index = 0; index < tuple.length; index++) {
680
+ const values = [...new Set(candidates.map((row) => row[index].toString()))].map(BigInt).sort(compareBigInt);
681
+ const result = narrowTermDomain(env, tuple[index], values);
682
+ if (!result.ok) return false;
683
+ changed ||= result.changed;
684
+ }
685
+ }
686
+ } else if (global.kind === 'globalCardinality') {
687
+ const values = global.terms.map((term) => expressionValue(term, env));
688
+ for (const { key, count: countTerm } of global.pairs) {
689
+ const known = values.filter((value) => value === key).length;
690
+ const possibleTerms = global.terms.filter((term, index) => {
691
+ if (values[index] != null) return false;
692
+ const resolved = deref(term, env);
693
+ const domain = resolved.type === VAR ? domainForRoot(resolved.name, env) : null;
694
+ return domain == null || domain.some((value) => value === key);
695
+ });
696
+ const countResult = narrowTermDomain(env, countTerm, integerRange(known, known + possibleTerms.length));
697
+ if (!countResult.ok) return false;
698
+ changed ||= countResult.changed;
699
+ const count = expressionValue(countTerm, env);
700
+ if (count === BigInt(known)) {
701
+ for (const term of possibleTerms) {
702
+ const resolved = deref(term, env);
703
+ if (resolved.type !== VAR) continue;
704
+ const domain = domainForRoot(resolved.name, env);
705
+ const result = narrowTermDomain(env, term, domain.filter((value) => value !== key));
706
+ if (!result.ok) return false;
707
+ changed ||= result.changed;
708
+ }
709
+ } else if (count === BigInt(known + possibleTerms.length)) {
710
+ for (const term of possibleTerms) {
711
+ const result = narrowTermDomain(env, term, [key]);
712
+ if (!result.ok) return false;
713
+ changed ||= result.changed;
714
+ }
715
+ }
716
+ }
717
+ if (global.cost != null && values.every((value) => value != null)) {
718
+ let total = 0n;
719
+ for (let row = 0; row < values.length; row++) {
720
+ const column = global.pairs.findIndex(({ key }) => key === values[row]);
721
+ if (column < 0) return false;
722
+ total += global.matrix[row][column];
723
+ }
724
+ const result = bindExpressionEquality(global.cost, numberTerm(total.toString()), env);
725
+ if (!result.ok) return false;
726
+ changed ||= result.changed;
727
+ }
728
+ } else if (global.kind === 'zcompare') {
729
+ const order = orderAtom(global.order, env);
730
+ const left = expressionValue(global.left, env);
731
+ const right = expressionValue(global.right, env);
732
+ if (left != null && right != null) {
733
+ const resolvedOrder = atom(comparedOrder(left, right));
734
+ if (order == null) {
735
+ if (!unify(global.order, resolvedOrder, env)) return false;
736
+ changed = true;
737
+ } else if (order !== resolvedOrder.name) return false;
738
+ } else if (order === '=') {
739
+ const result = bindLinearEquality(global.left, global.right, env);
740
+ if (!result.ok) return false;
741
+ changed ||= result.changed;
742
+ }
743
+ } else if (global.kind === 'element') {
414
744
  const index = expressionValue(global.index, env);
415
745
  if (index != null) {
416
746
  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');
@@ -1545,6 +1546,19 @@ contradiction :- Z in 1..3, Z = 4.
1545
1546
  'answer(1, 4, 1).\nanswer(2, 3, 0).\n', 'CLP(Z) constrained answers');
1546
1547
  },
1547
1548
  },
1549
+ {
1550
+ name: 'CLP(Z) finite global constraints match the portable example',
1551
+ run: () => {
1552
+ const filename = path.join(packageRoot, 'examples', 'clpz-global-constraints.pl');
1553
+ const source = fs.readFileSync(filename, 'utf8');
1554
+ const program = Program.parseSources([{ text: source, filename }]);
1555
+ assertEqual(program.findGroup('tuples_in', 2)?.module, 'clpz', 'tuples_in/2 module');
1556
+ assertEqual(program.findGroup('global_cardinality', 3)?.module, 'clpz', 'global_cardinality/3 module');
1557
+ assertEqual(program.findGroup('circuit', 1)?.module, 'clpz', 'circuit/1 module');
1558
+ const expected = fs.readFileSync(path.join(packageRoot, 'examples', 'output', 'clpz-global-constraints.pl'), 'utf8');
1559
+ assertEqual(run(program, { goal: 'advanced_clpz(X0, X1)' }).stdout, expected, 'global constraint answers');
1560
+ },
1561
+ },
1548
1562
  {
1549
1563
  name: 'Part 2 modules isolate predicates and support selective imports',
1550
1564
  run: () => {
@@ -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 **210 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,6 +6414,7 @@ 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) |
6416
6420
  | [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) |