eyeprolog 1.1.18 → 1.1.20

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.
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
11
11
  | builtins | 11 | 0 | 0 | 0 | 11 |
12
12
  | context | 11 | 0 | 0 | 0 | 11 |
13
13
  | control | 15 | 0 | 0 | 0 | 15 |
14
- | iso | 164 | 209 | 0 | 0 | 373 |
14
+ | iso | 165 | 209 | 0 | 0 | 374 |
15
15
  | lists | 52 | 3 | 0 | 0 | 55 |
16
16
  | modules | 2 | 0 | 0 | 0 | 2 |
17
17
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
23
23
  | terms | 26 | 3 | 0 | 0 | 29 |
24
24
  | unification | 18 | 0 | 0 | 0 | 18 |
25
25
  | variables | 16 | 9 | 0 | 0 | 25 |
26
- | **Total** | **479** | **260** | **19** | **21** | **779** |
26
+ | **Total** | **480** | **260** | **19** | **21** | **780** |
@@ -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 @@
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.18",
6
+ "version": "1.1.20",
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
@@ -468,6 +468,7 @@
468
468
  "clpz-global-constraints",
469
469
  "clpz-n-queens",
470
470
  "clpz-resource-allocation",
471
+ "clpz-sudoku-9x9",
471
472
  "collatz-1000",
472
473
  "combinatorics-findall-sort",
473
474
  "competitive-enzyme-kinetics",
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) {
@@ -193,23 +195,28 @@ function rootVariableName(term, env) {
193
195
  return term.type === VAR ? term.name : null;
194
196
  }
195
197
 
196
- function domainsForRoot(root, env) {
197
- const domains = [];
198
- for (const [name, values] of storeOf(env).domains) {
199
- 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));
200
208
  }
201
- return domains;
209
+ domainCacheByState.set(env._state, { domains: store.domains, roots });
210
+ return roots;
202
211
  }
203
212
 
204
213
  function intersectValues(left, right) {
205
- const allowed = new Set(right.map(String));
206
- return left.filter((value) => allowed.has(String(value)));
214
+ const allowed = new Set(right);
215
+ return left.filter((value) => allowed.has(value));
207
216
  }
208
217
 
209
218
  function domainForRoot(root, env) {
210
- const domains = domainsForRoot(root, env);
211
- if (domains.length === 0) return null;
212
- return domains.slice(1).reduce(intersectValues, domains[0]);
219
+ return domainsByRoot(env).get(root) ?? null;
213
220
  }
214
221
 
215
222
  function constrainTermDomain(next, term, values) {
@@ -230,9 +237,14 @@ function narrowTermDomain(next, term, values) {
230
237
  const changed = existing == null || existing.length !== narrowed.length ||
231
238
  existing.some((value, index) => value !== narrowed[index]);
232
239
  if (!changed) return { ok: true, changed: false };
233
- const domains = new Map(storeOf(next).domains);
234
- domains.set(resolved.name, narrowed);
235
- updateStore(next, { domains });
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
+ }
236
248
  if (narrowed.length === 1 && !unify(resolved, numberTerm(narrowed[0].toString()), next)) {
237
249
  return { ok: false, changed: false };
238
250
  }
@@ -650,9 +662,143 @@ function bindLinearEquality(left, right, env) {
650
662
  return { ok: unify(variable(name), numberTerm(value.toString()), env), changed: true };
651
663
  }
652
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
+
653
786
  function propagate(env) {
654
787
  const store = env._clpz;
655
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;
656
802
  let changed;
657
803
  do {
658
804
  changed = false;
@@ -665,7 +811,11 @@ function propagate(env) {
665
811
  }
666
812
  }
667
813
  for (const global of store.globals) {
668
- if (global.kind === 'nvalue') {
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') {
669
819
  const values = global.terms.map((term) => expressionValue(term, env));
670
820
  const known = new Set(values.filter((value) => value != null).map(String));
671
821
  const unresolved = values.filter((value) => value == null).length;
package/src/iso.js CHANGED
@@ -1814,7 +1814,7 @@ function* catchBuiltin({ solver, goal, env }) {
1814
1814
  const ball = error instanceof ThrownTerm
1815
1815
  ? error.term
1816
1816
  : error instanceof PrologError
1817
- ? formalErrorTerm(error)
1817
+ ? freshCopy(formalErrorTerm(error), new Env())
1818
1818
  : null;
1819
1819
  if (ball == null) throw error;
1820
1820
  const recovered = env.clone();
@@ -1827,7 +1827,9 @@ function* catchBuiltin({ solver, goal, env }) {
1827
1827
  function* throwBuiltin({ goal, env }) {
1828
1828
  const ball = deref(goal.args[0], env);
1829
1829
  if (ball.type === VAR) throw new PrologError('instantiation_error');
1830
- throw new ThrownTerm(copyResolved(ball, env));
1830
+ // ISO throw/1 copies the thrown term before control unwinds. Freshen
1831
+ // variables so the catcher cannot retain aliases to the protected goal.
1832
+ throw new ThrownTerm(freshCopy(ball, env));
1831
1833
  }
1832
1834
  function* onceBuiltin({ solver, goal, env }) {
1833
1835
  const child = solver.cloneForInnerGoal(1);
package/src/repl.js CHANGED
@@ -57,7 +57,7 @@ export async function runRepl(engine, options = {}) {
57
57
  exitCode = error.code;
58
58
  break;
59
59
  }
60
- output.write(` ${formatError(error)}\n`);
60
+ output.write(` ${formatError(engine, state, error)}\n`);
61
61
  }
62
62
  }
63
63
  } catch (error) {
@@ -368,13 +368,15 @@ function queryVariables(goal) {
368
368
 
369
369
  function formatAnswer(engine, state, variables, env) {
370
370
  const bindings = [];
371
+ const queryVariableNames = new Set(variables.map((variable) => variable.name));
371
372
  const names = new Map(variables.map((variable) => [variable.name, variable.name]));
372
373
  let generated = 0;
373
374
 
374
375
  for (const variable of variables) collectUnboundVariables(engine, variable, env, names, () => `_${letterName(generated++)}`);
375
376
  for (const variable of variables) {
376
377
  const value = engine.deref(variable, env);
377
- if (value.type === 'var' && value.name === variable.name) continue;
378
+ if (value.type === 'var' &&
379
+ (value.name === variable.name || !queryVariableNames.has(value.name))) continue;
378
380
  bindings.push(`${variable.name} = ${engine.formatTermForWrite(value, env, {
379
381
  quoted: true,
380
382
  operators: [...state.program.operators.values()],
@@ -406,7 +408,21 @@ function letterName(index) {
406
408
  return suffix === 0 ? letter : `${letter}${suffix}`;
407
409
  }
408
410
 
409
- function formatError(error) {
411
+ function formatError(engine, state, error) {
412
+ if (error?.name === 'PrologError') {
413
+ const env = new engine.Env();
414
+ const variableNames = new Map();
415
+ let generated = 0;
416
+ if (error.culprit != null) {
417
+ collectUnboundVariables(engine, error.culprit, env, variableNames, () => `_${letterName(generated++)}`);
418
+ }
419
+ const culprit = error.culprit == null ? '' : `, ${engine.formatTermForWrite(error.culprit, env, {
420
+ quoted: true,
421
+ operators: [...state.program.operators.values()],
422
+ variableNames,
423
+ })}`;
424
+ return `error(${error.formal}${culprit}).`;
425
+ }
410
426
  const message = error?.message ?? String(error);
411
427
  return message.endsWith('.') ? message : `${message}.`;
412
428
  }
@@ -9,7 +9,7 @@ case totals.
9
9
  | --- | --- | --- |
10
10
  | Clause 6 lexical and term syntax | tokenizer, operator parser, lists, curly terms, quotes, numeric syntax, comments | `scryer_lexical_terms`, `lexical_and_curly_terms`, `double_quoted_lists`, `corrigendum1_double_quote_operator`, syntax error cases |
11
11
  | Clause 7 term order and unification | finite-tree unification, identity, standard order, errors | `unification_control_information`, `swipl_occurs_check`, `term_modes_and_ordering`, `logtalk_compare_standard_order` |
12
- | Clause 7 control and exceptions | call, cut, conjunction, disjunction, if-then-else, catch and throw | `cut_control`, `control_and_terms`, `exceptions_and_flags`, `corrigenda_catch_callability` |
12
+ | Clause 7 control and exceptions | call, cut, conjunction, disjunction, if-then-else, catch and throw | `cut_control`, `control_and_terms`, `exceptions_and_flags`, `corrigenda_catch_callability`, `throw_copies_ball` |
13
13
  | 8.2-8.5 term predicates | unification, Corrigendum 2 tests, comparison, sorting, creation and decomposition | `corrigenda_term_predicates`, `corrigenda_sort_keysort`, `logtalk_arg_unification`, `logtalk_univ`, associated error cases |
14
14
  | 8.6-8.7 arithmetic predicates | `is/2` and all six arithmetic comparisons | `arithmetic`, comparison cases, isolated evaluation errors |
15
15
  | 8.8-8.10 database and solutions | logical update view, dynamic mutation, all-solutions grouping | `dynamic_database`, `trealla_logical_update_view`, `corrigenda_retractall`, `grouped_solutions_and_clauses` |
@@ -94,7 +94,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
94
94
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
95
95
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
96
96
 
97
- The corpus has 373 cases in `iso/` and 779 file-based conformance cases in
97
+ The corpus has 374 cases in `iso/` and 780 file-based conformance cases in
98
98
  total. The generated `conformance-report.md` is the authoritative source for
99
99
  current category totals. Together with regression, documentation-sync, API,
100
100
  example, and book-example checks, `npm test` is the release gate.
@@ -0,0 +1,17 @@
1
+ % ISO 7.12.1: throw/1 copies the ball before searching for a catcher.
2
+ %% goal: user_throw_copy
3
+ %% goal: error_throw_copy
4
+
5
+ user_throw_copy :-
6
+ catch(throw(g(X)), g(V), true),
7
+ X = protected,
8
+ V = caught.
9
+
10
+ error_throw_copy :-
11
+ catch(
12
+ number_chars(_, [1, [], X|2]),
13
+ error(type_error(list, Culprit), _),
14
+ true
15
+ ),
16
+ X = protected,
17
+ Culprit = [1, [], caught|2].
@@ -0,0 +1,2 @@
1
+ user_throw_copy.
2
+ error_throw_copy.
@@ -496,6 +496,32 @@ c4 ?- call((!;1)).
496
496
  assertEqual(result.stderr, '', 'stderr');
497
497
  },
498
498
  },
499
+ {
500
+ name: 'REPL hides aliases to fresh throw variables while preserving query aliases',
501
+ run: () => {
502
+ const result = runCli([], {
503
+ input: 'catch(throw(g(X)),g(V),true).\nX = Y.\nhalt.\n',
504
+ });
505
+ assertEqual(result.status, 0, 'exit status');
506
+ assertEqual(result.stdout, '?- true.\n?- X = Y.\n?- ', 'stdout');
507
+ assertEqual(result.stderr, '', 'stderr');
508
+ },
509
+ },
510
+ {
511
+ name: 'REPL freshens variables displayed in uncaught ISO errors',
512
+ run: () => {
513
+ const result = runCli([], {
514
+ input: 'number_chars(V,[1,[],X|2]).\nnumber_chars(V,[1,[],Xx|2]).\nhalt.\n',
515
+ });
516
+ assertEqual(result.status, 0, 'exit status');
517
+ assertEqual(result.stdout,
518
+ '?- error(type_error(list), [1, [], _A | 2]).\n' +
519
+ '?- error(type_error(list), [1, [], _A | 2]).\n' +
520
+ '?- ',
521
+ 'stdout');
522
+ assertEqual(result.stderr, '', 'stderr');
523
+ },
524
+ },
499
525
  {
500
526
  name: 'REPL enumerates and stops answers like the Scryer top level',
501
527
  run: () => {
@@ -1540,10 +1566,13 @@ answer(X, Y, B) :-
1540
1566
  B #<==> X #= 1,
1541
1567
  labeling([ff, down], [X, Y, B]).
1542
1568
  contradiction :- Z in 1..3, Z = 4.
1569
+ pruned(Domain) :- [X, Y] ins 1..3, all_distinct([X, Y]), X #= 2, fd_dom(Y, Domain).
1570
+ hall(Domain) :- [X, Y] ins 1..2, Z in 1..3, all_distinct([X, Y, Z]), fd_dom(Z, Domain).
1571
+ repeated(X) :- X in 1..3, all_distinct([X, X]).
1543
1572
  `);
1544
1573
  assertEqual(program.findGroup('labeling', 2)?.module, 'clpz', 'labeling/2 module');
1545
- assertEqual(run(program, { goals: ['answer(X, Y, B)', 'contradiction'] }).stdout,
1546
- 'answer(1, 4, 1).\nanswer(2, 3, 0).\n', 'CLP(Z) constrained answers');
1574
+ assertEqual(run(program, { goals: ['answer(X, Y, B)', 'contradiction', 'pruned(Domain)', 'hall(Domain)', 'repeated(X)'] }).stdout,
1575
+ 'answer(1, 4, 1).\nanswer(2, 3, 0).\npruned(1 \\/ 3).\nhall(3).\n', 'CLP(Z) constrained answers');
1547
1576
  },
1548
1577
  },
1549
1578
  {
@@ -5749,8 +5749,14 @@ instances whose message contains the corresponding Prolog error term.
5749
5749
  | *syntax_error(number)*, *syntax_error(read_term)* | Lexical number conversion or streamed term parsing failed. |
5750
5750
 
5751
5751
  `catch/3` converts a `PrologError` into a catchable
5752
- `error(Formal,eyeprolog)` term. A user ball from `throw/1` is delivered as-is.
5753
- An unmatched ball or error continues outward.
5752
+ `error(Formal,eyeprolog)` term. `throw/1` copies its ball before unwinding: bound
5753
+ parts are preserved, repeated variables remain shared within the copied ball,
5754
+ and unbound variables are fresh with respect to the protected goal and catcher.
5755
+ Catchable error terms follow the same variable-freshening rule. At the interactive
5756
+ top level, variables that occur in an uncaught ISO error are rendered with fresh
5757
+ answer names such as `_A` rather than reusing query-variable spellings such as
5758
+ `X` or `Xx`; this keeps the displayed error consistent with the copied exception
5759
+ term. An unmatched ball or error continues outward.
5754
5760
 
5755
5761
  Streams belong to one solver run and are shared by nested calls, exceptions,
5756
5762
  and solution collectors. `user_input` and `user_output` are always present.
@@ -6365,7 +6371,7 @@ Review questions:
6365
6371
  </figure>
6366
6372
 
6367
6373
  The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
6368
- top-level directory contains **210 self-contained runnable programs**. Every
6374
+ top-level directory contains **211 self-contained runnable programs**. Every
6369
6375
  source program has an exact answer file under
6370
6376
  [examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
6371
6377
  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
@@ -6417,6 +6423,7 @@ mode at a time.
6417
6423
  | [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) |
6418
6424
  | [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) |
6419
6425
  | [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) |
6426
+ | [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) |
6420
6427
  | [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) |
6421
6428
  | [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) |
6422
6429
  | [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) |
@@ -6814,7 +6821,7 @@ node test/run-conformance-report.mjs
6814
6821
  ```
6815
6822
 
6816
6823
  The complete suite must pass before release. The file-based conformance corpus
6817
- contains 779 cases, including 373 focused ISO
6824
+ contains 780 cases, including 374 focused ISO
6818
6825
  cases derived from the success, failure, mode, and error behavior in
6819
6826
  ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
6820
6827
  Separate exact-output suites check 189 normal