eyeprolog 1.1.18 → 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/examples/clpz-sudoku-9x9.pl +64 -0
- package/examples/output/clpz-sudoku-9x9.pl +1 -0
- package/package.json +1 -1
- package/playground.html +1 -0
- package/src/clpz.js +164 -14
- package/test/run-regression.mjs +5 -2
- package/the-art-of-eyeprolog.md +2 -1
|
@@ -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
package/playground.html
CHANGED
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
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
206
|
-
return left.filter((value) => allowed.has(
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 === '
|
|
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/test/run-regression.mjs
CHANGED
|
@@ -1540,10 +1540,13 @@ answer(X, Y, B) :-
|
|
|
1540
1540
|
B #<==> X #= 1,
|
|
1541
1541
|
labeling([ff, down], [X, Y, B]).
|
|
1542
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]).
|
|
1543
1546
|
`);
|
|
1544
1547
|
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');
|
|
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');
|
|
1547
1550
|
},
|
|
1548
1551
|
},
|
|
1549
1552
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6365,7 +6365,7 @@ Review questions:
|
|
|
6365
6365
|
</figure>
|
|
6366
6366
|
|
|
6367
6367
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
6368
|
-
top-level directory contains **
|
|
6368
|
+
top-level directory contains **211 self-contained runnable programs**. Every
|
|
6369
6369
|
source program has an exact answer file under
|
|
6370
6370
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
|
|
6371
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
|
|
@@ -6417,6 +6417,7 @@ mode at a time.
|
|
|
6417
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) |
|
|
6418
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) |
|
|
6419
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) |
|
|
6420
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) |
|
|
6421
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) |
|
|
6422
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) |
|