eyeprolog 1.5.108 → 1.6.0
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 +3 -97
- package/examples/output/clpz-sudoku-9x9.pl +1 -2
- package/package.json +1 -1
- package/src/ARCHITECTURE.md +6 -3
- package/src/clpb-host.js +48 -0
- package/src/clpz-host.js +371 -0
- package/src/lib/clpb.pl +1 -6
- package/src/lib/clpz.pl +6 -1
- package/src/solver.js +3 -0
- package/src/standard-library.js +4 -0
- package/test/regression/cases-api.mjs +23 -2
- package/the-art-of-eyeprolog.md +2 -2
|
@@ -4,50 +4,10 @@
|
|
|
4
4
|
% declarative search relation: every row, column, and 3-by-3 block is
|
|
5
5
|
% all-distinct and first-fail labeling searches the remaining cells.
|
|
6
6
|
%
|
|
7
|
-
%
|
|
8
|
-
%
|
|
9
|
-
% actually running it (~4 minutes of real search on this machine, no
|
|
10
|
-
% known-answer shortcut). It is left out of the default %% goal: examples
|
|
11
|
-
% below purely because 4 minutes per run is too slow for the example
|
|
12
|
-
% suite, which re-runs on every test invocation, not because of any
|
|
13
|
-
% propagation weakness.
|
|
14
|
-
%
|
|
15
|
-
% sudoku9_witness/1 below does NOT search. It unifies Rows with the
|
|
16
|
-
% well-known published AI Escargot answer FIRST and only then calls
|
|
17
|
-
% sudoku9/1, so every cell is already ground before labeling/2 ever runs --
|
|
18
|
-
% it is a fast constraint check on a known-good grid, not a demonstration of
|
|
19
|
-
% solving the puzzle, and it must never be read as one. It exists so the
|
|
20
|
-
% example suite still exercises the sudoku9_* constraint-model code on every
|
|
21
|
-
% run, without paying the multi-minute real-search cost.
|
|
22
|
-
%
|
|
23
|
-
% sudoku4_solution/1 is a 4-by-4 "Shidoku" (2-by-2 blocks, digits 1-4) built
|
|
24
|
-
% from the identical relational model, actually solved from its blanks by
|
|
25
|
-
% the same labeling call, so the example suite also exercises real search
|
|
26
|
-
% (just at a size that finishes instantly) on every run.
|
|
27
|
-
%
|
|
28
|
-
% Readers who want to watch the real 9-by-9 search can call sudoku9(Rows)
|
|
29
|
-
% directly with an unbound Rows; expect it to take a few minutes.
|
|
30
|
-
|
|
31
|
-
%% goal: sudoku4_solution(X0)
|
|
32
|
-
%% goal: sudoku9_witness(X0)
|
|
33
|
-
|
|
34
|
-
sudoku4_solution(Rows) :-
|
|
35
|
-
Rows = [
|
|
36
|
-
[_, _, 3, _],
|
|
37
|
-
[_, 4, _, 2],
|
|
38
|
-
[2, 1, _, _],
|
|
39
|
-
[_, _, 2, _]
|
|
40
|
-
],
|
|
41
|
-
sudoku4_rows(Rows),
|
|
42
|
-
sudoku4_transpose(Rows, Columns),
|
|
43
|
-
sudoku4_rows_distinct(Columns),
|
|
44
|
-
sudoku4_blocks(Rows),
|
|
45
|
-
sudoku4_flatten(Rows, Cells),
|
|
46
|
-
labeling([ff], Cells).
|
|
7
|
+
% The default goal genuinely searches AI Escargot from its clues and finds
|
|
8
|
+
% the unique solution. No known-answer shortcut is used before labeling.
|
|
47
9
|
|
|
48
|
-
|
|
49
|
-
sudoku9_known_solution(Rows),
|
|
50
|
-
sudoku9(Rows).
|
|
10
|
+
%% goal: sudoku9(X0)
|
|
51
11
|
|
|
52
12
|
sudoku9(Rows) :-
|
|
53
13
|
Rows = [
|
|
@@ -68,18 +28,6 @@ sudoku9(Rows) :-
|
|
|
68
28
|
sudoku9_flatten(Rows, Cells),
|
|
69
29
|
labeling([ff], Cells).
|
|
70
30
|
|
|
71
|
-
sudoku9_known_solution([
|
|
72
|
-
[1, 6, 2, 8, 5, 7, 4, 9, 3],
|
|
73
|
-
[5, 3, 4, 1, 2, 9, 6, 7, 8],
|
|
74
|
-
[7, 8, 9, 6, 4, 3, 5, 2, 1],
|
|
75
|
-
[4, 7, 5, 3, 1, 2, 9, 8, 6],
|
|
76
|
-
[9, 1, 3, 5, 8, 6, 7, 4, 2],
|
|
77
|
-
[6, 2, 8, 7, 9, 4, 1, 3, 5],
|
|
78
|
-
[3, 5, 6, 4, 7, 8, 2, 1, 9],
|
|
79
|
-
[2, 4, 1, 9, 3, 5, 8, 6, 7],
|
|
80
|
-
[8, 9, 7, 2, 6, 1, 3, 5, 4]
|
|
81
|
-
]).
|
|
82
|
-
|
|
83
31
|
sudoku9_rows([]).
|
|
84
32
|
sudoku9_rows([Row|Rows]) :-
|
|
85
33
|
Row ins 1..9,
|
|
@@ -117,45 +65,3 @@ sudoku9_flatten([Row|Rows], Cells) :-
|
|
|
117
65
|
|
|
118
66
|
sudoku9_append([], Ys, Ys).
|
|
119
67
|
sudoku9_append([X|Xs], Ys, [X|Zs]) :- sudoku9_append(Xs, Ys, Zs).
|
|
120
|
-
|
|
121
|
-
% sudoku4_*/N below mirror the sudoku9_*/N relations at 4-by-4 scale (2-by-2
|
|
122
|
-
% blocks instead of 3-by-3), so sudoku4_solution/1 exercises the identical
|
|
123
|
-
% relational model as sudoku9/1, just small enough to search instantly.
|
|
124
|
-
|
|
125
|
-
sudoku4_rows([]).
|
|
126
|
-
sudoku4_rows([Row|Rows]) :-
|
|
127
|
-
Row ins 1..4,
|
|
128
|
-
all_distinct(Row),
|
|
129
|
-
sudoku4_rows(Rows).
|
|
130
|
-
|
|
131
|
-
sudoku4_rows_distinct([]).
|
|
132
|
-
sudoku4_rows_distinct([Row|Rows]) :-
|
|
133
|
-
all_distinct(Row),
|
|
134
|
-
sudoku4_rows_distinct(Rows).
|
|
135
|
-
|
|
136
|
-
sudoku4_transpose([[]|_], []).
|
|
137
|
-
sudoku4_transpose(Rows, [Column|Columns]) :-
|
|
138
|
-
sudoku4_heads_tails(Rows, Column, Tails),
|
|
139
|
-
sudoku4_transpose(Tails, Columns).
|
|
140
|
-
|
|
141
|
-
sudoku4_heads_tails([], [], []).
|
|
142
|
-
sudoku4_heads_tails([[Head|Tail]|Rows], [Head|Heads], [Tail|Tails]) :-
|
|
143
|
-
sudoku4_heads_tails(Rows, Heads, Tails).
|
|
144
|
-
|
|
145
|
-
sudoku4_blocks([]).
|
|
146
|
-
sudoku4_blocks([A, B|Rows]) :-
|
|
147
|
-
sudoku4_block_row(A, B),
|
|
148
|
-
sudoku4_blocks(Rows).
|
|
149
|
-
|
|
150
|
-
sudoku4_block_row([], []).
|
|
151
|
-
sudoku4_block_row([A, B|As], [C, D|Bs]) :-
|
|
152
|
-
all_distinct([A, B, C, D]),
|
|
153
|
-
sudoku4_block_row(As, Bs).
|
|
154
|
-
|
|
155
|
-
sudoku4_flatten([], []).
|
|
156
|
-
sudoku4_flatten([Row|Rows], Cells) :-
|
|
157
|
-
sudoku4_append(Row, Rest, Cells),
|
|
158
|
-
sudoku4_flatten(Rows, Rest).
|
|
159
|
-
|
|
160
|
-
sudoku4_append([], Ys, Ys).
|
|
161
|
-
sudoku4_append([X|Xs], Ys, [X|Zs]) :- sudoku4_append(Xs, Ys, Zs).
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
sudoku9_witness([[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]]).
|
|
1
|
+
sudoku9([[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/src/ARCHITECTURE.md
CHANGED
|
@@ -91,9 +91,12 @@ extra dispatch in solver hot paths. Existing scalar/indexed solver paths stay
|
|
|
91
91
|
as direct function calls. Candidate indexing is separated physically but
|
|
92
92
|
retains the same data structures and selection functions.
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
Generic solver fast paths remain co-located in `solver.js` until a split can
|
|
95
|
+
demonstrate performance parity. Module-specific acceleration belongs to the
|
|
96
|
+
matching `*-host.js` adapter: `clpz-host.js` and `clpb-host.js`, for example,
|
|
97
|
+
construct native propagation/search plans while their Prolog modules retain
|
|
98
|
+
validation, residual constraints, and portable fallbacks. A cleaner file layout
|
|
99
|
+
is not worth a runtime regression, so such splits must retain benchmark parity.
|
|
97
100
|
|
|
98
101
|
Performance claims use `npm test`'s own elapsed time across the full corpus,
|
|
99
102
|
not predicate, inference, or host-call counts as a substitute for elapsed
|
package/src/clpb-host.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Native search-plan construction for library(clpb). Formula construction and
|
|
2
|
+
// BDD propagation remain in clpb.pl; this adapter replaces its interpreted
|
|
3
|
+
// variable sorting and Boolean enumeration with an equivalent goal tree.
|
|
4
|
+
|
|
5
|
+
import { COMPOUND, NUMBER, VAR, atom, compound, deref, numberTerm, properListItems } from './term.js';
|
|
6
|
+
|
|
7
|
+
function conjunction(goals) {
|
|
8
|
+
if (goals.length === 0) return atom('true');
|
|
9
|
+
let result = goals[goals.length - 1];
|
|
10
|
+
for (let index = goals.length - 2; index >= 0; index--) result = compound(',', [goals[index], result]);
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function variableIndex(term, env) {
|
|
15
|
+
const wrapper = env.getPrologAttribute(term.name, 'clpb', 'clpb', 1);
|
|
16
|
+
const attribute = wrapper == null ? null : deref(wrapper.args[0], env);
|
|
17
|
+
if (attribute?.type !== COMPOUND || attribute.name !== 'index_root' || attribute.arity !== 2) return 0n;
|
|
18
|
+
const index = deref(attribute.args[0], env);
|
|
19
|
+
return index.type === NUMBER ? BigInt(index.name) : 0n;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function expandLabeling({ solver, goal, env }) {
|
|
23
|
+
const items = properListItems(goal.args[0], env) ?? [];
|
|
24
|
+
const variables = [];
|
|
25
|
+
for (let position = 0; position < items.length; position++) {
|
|
26
|
+
const value = deref(items[position], env);
|
|
27
|
+
if (value.type === VAR) variables.push({ term: value, index: variableIndex(value, env), position });
|
|
28
|
+
}
|
|
29
|
+
variables.sort((left, right) => left.index < right.index ? -1 : left.index > right.index ? 1 : left.position - right.position);
|
|
30
|
+
solver.stats.clpb_native_labelings++;
|
|
31
|
+
return conjunction(variables.map(({ term }) => compound(';', [
|
|
32
|
+
compound('=', [term, numberTerm(0)]),
|
|
33
|
+
compound('=', [term, numberTerm(1)]),
|
|
34
|
+
])));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function* unreachable() {
|
|
38
|
+
throw new Error('CLP adapter expansion was not applied');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const clpbHostBuiltins = {
|
|
42
|
+
register(registry) {
|
|
43
|
+
registry.add('eyeprolog__clpb_labeling', 1, unreachable, {
|
|
44
|
+
expandGoal: expandLabeling,
|
|
45
|
+
eyePrologLibrary: true,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
package/src/clpz-host.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// Native acceleration for the finite all_distinct subset of library(clpz).
|
|
2
|
+
// The public predicates, validation, and general fallbacks remain in clpz.pl;
|
|
3
|
+
// these private adapters only replace the expensive matching and search loops
|
|
4
|
+
// when the live attributed-variable store is completely understood here.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ATOM, COMPOUND, NUMBER, VAR, atom, compound, deref, isDecimalInteger,
|
|
8
|
+
numberTerm, properListItems, variable,
|
|
9
|
+
} from './term.js';
|
|
10
|
+
|
|
11
|
+
let distinctStateId = 0;
|
|
12
|
+
|
|
13
|
+
function qualified(name, args) {
|
|
14
|
+
const goal = compound(name, args);
|
|
15
|
+
goal.module = 'clpz';
|
|
16
|
+
return goal;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function conjunction(goals) {
|
|
20
|
+
if (goals.length === 0) return atom('true');
|
|
21
|
+
let result = goals[goals.length - 1];
|
|
22
|
+
for (let index = goals.length - 2; index >= 0; index--) {
|
|
23
|
+
result = compound(',', [goals[index], result]);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function finiteDomainValues(domain, env, output) {
|
|
29
|
+
const resolved = deref(domain, env);
|
|
30
|
+
if (resolved?.type !== COMPOUND) return false;
|
|
31
|
+
if (resolved.name === 'from_to' && resolved.arity === 2) {
|
|
32
|
+
const lower = deref(resolved.args[0], env);
|
|
33
|
+
const upper = deref(resolved.args[1], env);
|
|
34
|
+
if (lower?.type !== COMPOUND || lower.name !== 'n' || lower.arity !== 1 ||
|
|
35
|
+
upper?.type !== COMPOUND || upper.name !== 'n' || upper.arity !== 1) return false;
|
|
36
|
+
const lowerNumber = deref(lower.args[0], env);
|
|
37
|
+
const upperNumber = deref(upper.args[0], env);
|
|
38
|
+
if (lowerNumber.type !== NUMBER || upperNumber.type !== NUMBER ||
|
|
39
|
+
!isDecimalInteger(lowerNumber.name) || !isDecimalInteger(upperNumber.name)) return false;
|
|
40
|
+
const first = BigInt(lowerNumber.name);
|
|
41
|
+
const last = BigInt(upperNumber.name);
|
|
42
|
+
if (last < first || last - first > 30n) return false;
|
|
43
|
+
for (let value = first; value <= last; value++) output.add(value);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (resolved.name === 'split' && resolved.arity === 3) {
|
|
47
|
+
return finiteDomainValues(resolved.args[1], env, output) &&
|
|
48
|
+
finiteDomainValues(resolved.args[2], env, output);
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clpzAttributeFor(name, env) {
|
|
54
|
+
const wrapper = env.getPrologAttribute(name, 'clpz', 'clpz', 1);
|
|
55
|
+
const attribute = wrapper == null ? null : deref(wrapper.args[0], env);
|
|
56
|
+
return attribute?.type === COMPOUND && attribute.name === 'clpz_attr' && attribute.arity === 6
|
|
57
|
+
? attribute
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function activeClpzPropagators(attribute, env) {
|
|
62
|
+
const props = deref(attribute.args[4], env);
|
|
63
|
+
if (props?.type !== COMPOUND || props.name !== 'fd_props' || props.arity !== 3) return null;
|
|
64
|
+
const result = [];
|
|
65
|
+
for (const list of props.args) {
|
|
66
|
+
const entries = properListItems(list, env);
|
|
67
|
+
if (entries == null) return null;
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const propagator = deref(entry, env);
|
|
70
|
+
if (propagator?.type !== COMPOUND || propagator.name !== 'propagator' || propagator.arity !== 2) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const state = deref(propagator.args[1], env);
|
|
74
|
+
if (state.type === ATOM && state.name === 'dead') continue;
|
|
75
|
+
const constraint = deref(propagator.args[0], env);
|
|
76
|
+
if (constraint?.type !== COMPOUND || !['pdistinct', 'pexclude'].includes(constraint.name)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
result.push(constraint);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function allDistinctMatchingExists(domains, ground, forcedIndex = -1, forcedValue = null) {
|
|
86
|
+
if (forcedIndex >= 0 && (ground.has(forcedValue) || !domains[forcedIndex].includes(forcedValue))) return false;
|
|
87
|
+
const matching = new Map();
|
|
88
|
+
if (forcedIndex >= 0) matching.set(forcedValue, forcedIndex);
|
|
89
|
+
|
|
90
|
+
const augment = (variableIndex, seen) => {
|
|
91
|
+
for (const value of domains[variableIndex]) {
|
|
92
|
+
if (ground.has(value) || seen.has(value)) continue;
|
|
93
|
+
seen.add(value);
|
|
94
|
+
const previous = matching.get(value);
|
|
95
|
+
if (previous == null || (previous !== forcedIndex && augment(previous, seen))) {
|
|
96
|
+
matching.set(value, variableIndex);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (let index = 0; index < domains.length; index++) {
|
|
104
|
+
if (index === forcedIndex) continue;
|
|
105
|
+
if (!augment(index, new Set())) return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function distinctExpansion(goal, env) {
|
|
111
|
+
const members = properListItems(goal.args[0], env);
|
|
112
|
+
if (members == null || members.length > 16) return null;
|
|
113
|
+
const variables = [];
|
|
114
|
+
const domains = [];
|
|
115
|
+
const variableNames = new Set();
|
|
116
|
+
const ground = new Set();
|
|
117
|
+
for (const member of members) {
|
|
118
|
+
const value = deref(member, env);
|
|
119
|
+
if (value.type === NUMBER && isDecimalInteger(value.name)) {
|
|
120
|
+
const integer = BigInt(value.name);
|
|
121
|
+
if (ground.has(integer)) return atom('false');
|
|
122
|
+
ground.add(integer);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (value.type !== VAR || variableNames.has(value.name)) return atom('false');
|
|
126
|
+
const attribute = clpzAttributeFor(value.name, env);
|
|
127
|
+
if (attribute == null) return null;
|
|
128
|
+
const values = new Set();
|
|
129
|
+
if (!finiteDomainValues(attribute.args[3], env, values) || values.size === 0 || values.size > 30) return null;
|
|
130
|
+
variableNames.add(value.name);
|
|
131
|
+
variables.push(value);
|
|
132
|
+
domains.push([...values]);
|
|
133
|
+
}
|
|
134
|
+
if (!allDistinctMatchingExists(domains, ground)) return atom('false');
|
|
135
|
+
|
|
136
|
+
const removals = [];
|
|
137
|
+
for (let index = 0; index < variables.length; index++) {
|
|
138
|
+
for (const value of domains[index]) {
|
|
139
|
+
if (ground.has(value) || !allDistinctMatchingExists(domains, ground, index, value)) {
|
|
140
|
+
removals.push([variables[index], value]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (removals.length === 0) return compound('=', [goal.args[1], goal.args[2]]);
|
|
145
|
+
const goals = [];
|
|
146
|
+
let state = goal.args[1];
|
|
147
|
+
for (let index = 0; index < removals.length; index++) {
|
|
148
|
+
const nextState = index + 1 === removals.length
|
|
149
|
+
? goal.args[2]
|
|
150
|
+
: variable(`__clpz_distinct_state_${++distinctStateId}`);
|
|
151
|
+
goals.push(qualified('neq_num', [removals[index][0], numberTerm(removals[index][1]), state, nextState]));
|
|
152
|
+
state = nextState;
|
|
153
|
+
}
|
|
154
|
+
return conjunction(goals);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function finiteAllDistinctModel(items, env) {
|
|
158
|
+
const variableNames = [];
|
|
159
|
+
const variableIndexes = new Map();
|
|
160
|
+
for (const item of items) {
|
|
161
|
+
const value = deref(item, env);
|
|
162
|
+
if (value.type === NUMBER && isDecimalInteger(value.name)) continue;
|
|
163
|
+
if (value.type !== VAR || variableIndexes.has(value.name)) return null;
|
|
164
|
+
variableIndexes.set(value.name, variableNames.length);
|
|
165
|
+
variableNames.push(value.name);
|
|
166
|
+
}
|
|
167
|
+
if (variableNames.length === 0 || env._prologAttributes?.size !== variableNames.length) return null;
|
|
168
|
+
|
|
169
|
+
const domainSets = [];
|
|
170
|
+
const constraints = new Map();
|
|
171
|
+
for (const name of variableNames) {
|
|
172
|
+
const modules = env._prologAttributes.get(name);
|
|
173
|
+
if (modules?.size !== 1 || !modules.has('clpz') || modules.get('clpz')?.size !== 1) return null;
|
|
174
|
+
const attribute = clpzAttributeFor(name, env);
|
|
175
|
+
if (attribute == null) return null;
|
|
176
|
+
const values = new Set();
|
|
177
|
+
if (!finiteDomainValues(attribute.args[3], env, values) || values.size === 0) return null;
|
|
178
|
+
domainSets.push(values);
|
|
179
|
+
const propagators = activeClpzPropagators(attribute, env);
|
|
180
|
+
if (propagators == null) return null;
|
|
181
|
+
for (const constraint of propagators) {
|
|
182
|
+
if (constraint.name !== 'pdistinct') continue;
|
|
183
|
+
const members = properListItems(constraint.args[0], env);
|
|
184
|
+
if (members == null) return null;
|
|
185
|
+
const keyParts = [];
|
|
186
|
+
const normalized = [];
|
|
187
|
+
for (const member of members) {
|
|
188
|
+
const value = deref(member, env);
|
|
189
|
+
if (value.type === VAR) {
|
|
190
|
+
const index = variableIndexes.get(value.name);
|
|
191
|
+
if (index == null) return null;
|
|
192
|
+
keyParts.push(`v${index}`);
|
|
193
|
+
normalized.push(index);
|
|
194
|
+
} else if (value.type === NUMBER && isDecimalInteger(value.name)) {
|
|
195
|
+
const integer = BigInt(value.name);
|
|
196
|
+
keyParts.push(`n${integer}`);
|
|
197
|
+
normalized.push(integer);
|
|
198
|
+
} else return null;
|
|
199
|
+
}
|
|
200
|
+
constraints.set(keyParts.join(','), normalized);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (constraints.size === 0) return null;
|
|
204
|
+
const universe = [...new Set(domainSets.flatMap((set) => [...set]))].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
205
|
+
if (universe.length === 0 || universe.length > 30) return null;
|
|
206
|
+
const bits = new Map(universe.map((value, index) => [value, 2 ** index]));
|
|
207
|
+
const domains = domainSets.map((set) => {
|
|
208
|
+
let mask = 0;
|
|
209
|
+
for (const value of set) mask += bits.get(value);
|
|
210
|
+
return mask;
|
|
211
|
+
});
|
|
212
|
+
const groups = [];
|
|
213
|
+
for (const members of constraints.values()) {
|
|
214
|
+
let groundMask = 0;
|
|
215
|
+
const variables = [];
|
|
216
|
+
const seenVariables = new Set();
|
|
217
|
+
for (const member of members) {
|
|
218
|
+
if (typeof member === 'number') {
|
|
219
|
+
if (seenVariables.has(member)) return null;
|
|
220
|
+
seenVariables.add(member);
|
|
221
|
+
variables.push(member);
|
|
222
|
+
} else {
|
|
223
|
+
const bit = bits.get(member);
|
|
224
|
+
if (bit == null || (groundMask & bit) !== 0) return null;
|
|
225
|
+
groundMask |= bit;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
groups.push({ variables, groundMask });
|
|
229
|
+
}
|
|
230
|
+
return { variableNames, universe, domains, groups };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function bitCount(mask) {
|
|
234
|
+
let count = 0;
|
|
235
|
+
while (mask !== 0) {
|
|
236
|
+
mask &= mask - 1;
|
|
237
|
+
count++;
|
|
238
|
+
}
|
|
239
|
+
return count;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function allDistinctMaskMatchingExists(domains, group, forcedIndex = -1, forcedBit = 0) {
|
|
243
|
+
if (forcedIndex >= 0 && ((domains[forcedIndex] & forcedBit) === 0 ||
|
|
244
|
+
(group.groundMask & forcedBit) !== 0)) return false;
|
|
245
|
+
const matching = new Map();
|
|
246
|
+
if (forcedIndex >= 0) matching.set(forcedBit, forcedIndex);
|
|
247
|
+
const augment = (variableIndex, seen) => {
|
|
248
|
+
let remaining = domains[variableIndex] & ~group.groundMask;
|
|
249
|
+
while (remaining !== 0) {
|
|
250
|
+
const bit = remaining & -remaining;
|
|
251
|
+
remaining &= ~bit;
|
|
252
|
+
if (seen.has(bit)) continue;
|
|
253
|
+
seen.add(bit);
|
|
254
|
+
const previous = matching.get(bit);
|
|
255
|
+
if (previous == null || (previous !== forcedIndex && augment(previous, seen))) {
|
|
256
|
+
matching.set(bit, variableIndex);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
};
|
|
262
|
+
for (const index of group.variables) {
|
|
263
|
+
if (index === forcedIndex) continue;
|
|
264
|
+
if (!augment(index, new Set())) return false;
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function propagateAllDistinct(domains, groups) {
|
|
270
|
+
let changed = true;
|
|
271
|
+
while (changed) {
|
|
272
|
+
changed = false;
|
|
273
|
+
for (const group of groups) {
|
|
274
|
+
if (!allDistinctMaskMatchingExists(domains, group)) return false;
|
|
275
|
+
for (const index of group.variables) {
|
|
276
|
+
const domain = domains[index];
|
|
277
|
+
let reduced = domain;
|
|
278
|
+
let remaining = domain;
|
|
279
|
+
while (remaining !== 0) {
|
|
280
|
+
const bit = remaining & -remaining;
|
|
281
|
+
remaining &= ~bit;
|
|
282
|
+
if ((group.groundMask & bit) !== 0 ||
|
|
283
|
+
!allDistinctMaskMatchingExists(domains, group, index, bit)) reduced &= ~bit;
|
|
284
|
+
}
|
|
285
|
+
if (reduced === 0) return false;
|
|
286
|
+
if (reduced !== domain) {
|
|
287
|
+
domains[index] = reduced;
|
|
288
|
+
changed = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function* finiteAllDistinctSearch(solver, model, domains, iterations) {
|
|
297
|
+
if (!propagateAllDistinct(domains, model.groups)) return;
|
|
298
|
+
let selected = -1;
|
|
299
|
+
let selectedSize = Infinity;
|
|
300
|
+
for (let index = 0; index < domains.length; index++) {
|
|
301
|
+
const size = bitCount(domains[index]);
|
|
302
|
+
if (size > 1 && size < selectedSize) {
|
|
303
|
+
selected = index;
|
|
304
|
+
selectedSize = size;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (selected < 0) {
|
|
308
|
+
yield domains;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if ((++iterations.value & 1023) === 0) solver.checkMemoryLimit(true);
|
|
312
|
+
const bit = domains[selected] & -domains[selected];
|
|
313
|
+
const assigned = domains.slice();
|
|
314
|
+
assigned[selected] = bit;
|
|
315
|
+
yield* finiteAllDistinctSearch(solver, model, assigned, iterations);
|
|
316
|
+
const rejected = domains.slice();
|
|
317
|
+
rejected[selected] &= ~bit;
|
|
318
|
+
yield* finiteAllDistinctSearch(solver, model, rejected, iterations);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function* nativeLabeling({ solver, goal, env }) {
|
|
322
|
+
const model = goal._clpzModel;
|
|
323
|
+
for (const solution of finiteAllDistinctSearch(solver, model, model.domains.slice(), { value: 0 })) {
|
|
324
|
+
const next = env.clone();
|
|
325
|
+
next._prologAttributes = null;
|
|
326
|
+
for (let index = 0; index < model.variableNames.length; index++) {
|
|
327
|
+
const valueIndex = 31 - Math.clz32(solution[index]);
|
|
328
|
+
next.bind(model.variableNames[index], numberTerm(model.universe[valueIndex]));
|
|
329
|
+
}
|
|
330
|
+
yield next;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function expandLabeling({ solver, goal, env }) {
|
|
335
|
+
const options = properListItems(goal.args[0], env);
|
|
336
|
+
const option = options?.length === 1 ? deref(options[0], env) : null;
|
|
337
|
+
const items = properListItems(goal.args[1], env);
|
|
338
|
+
const model = option?.type === ATOM && option.name === 'ff' && items != null
|
|
339
|
+
? finiteAllDistinctModel(items, env)
|
|
340
|
+
: null;
|
|
341
|
+
if (model == null) return qualified('labeling_portable', goal.args);
|
|
342
|
+
solver.stats.clpz_native_labelings++;
|
|
343
|
+
const native = compound('eyeprolog__clpz_labeling_native', goal.args);
|
|
344
|
+
native._clpzModel = model;
|
|
345
|
+
return native;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function expandDistinct({ solver, goal, env }) {
|
|
349
|
+
const expanded = distinctExpansion(goal, env);
|
|
350
|
+
if (expanded == null) return qualified('distinct_portable', goal.args);
|
|
351
|
+
solver.stats.clpz_native_distinct_propagations++;
|
|
352
|
+
return expanded;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function* unreachable() {
|
|
356
|
+
throw new Error('CLP adapter expansion was not applied');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export const clpzHostBuiltins = {
|
|
360
|
+
register(registry) {
|
|
361
|
+
registry.add('eyeprolog__clpz_labeling', 2, unreachable, {
|
|
362
|
+
expandGoal: expandLabeling,
|
|
363
|
+
eyePrologLibrary: true,
|
|
364
|
+
});
|
|
365
|
+
registry.add('eyeprolog__clpz_labeling_native', 2, nativeLabeling, { eyePrologLibrary: true });
|
|
366
|
+
registry.add('eyeprolog__clpz_distinct', 3, unreachable, {
|
|
367
|
+
expandGoal: expandDistinct,
|
|
368
|
+
eyePrologLibrary: true,
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
};
|
package/src/lib/clpb.pl
CHANGED
|
@@ -1399,8 +1399,7 @@ node_ite(Node, Node-ite(Var,High,Low)) :-
|
|
|
1399
1399
|
labeling(Vs0) :-
|
|
1400
1400
|
must_be(list, Vs0),
|
|
1401
1401
|
maplist(labeling_var, Vs0),
|
|
1402
|
-
|
|
1403
|
-
maplist(indomain, Vs).
|
|
1402
|
+
eyeprolog__clpb_labeling(Vs0).
|
|
1404
1403
|
|
|
1405
1404
|
labeling_var(V) :- var(V), !.
|
|
1406
1405
|
labeling_var(V) :- V == 0, !.
|
|
@@ -1417,10 +1416,6 @@ var_with_index(V, I-V) :-
|
|
|
1417
1416
|
; I = 0
|
|
1418
1417
|
).
|
|
1419
1418
|
|
|
1420
|
-
indomain(0).
|
|
1421
|
-
indomain(1).
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
1419
|
%% sat_count(+Expr, -Count) is det.
|
|
1425
1420
|
%
|
|
1426
1421
|
% Count the number of admissible assignments. Count is the number of
|
package/src/lib/clpz.pl
CHANGED
|
@@ -1877,6 +1877,9 @@ labeling(Options, Vars) :-
|
|
|
1877
1877
|
must_be(list, labeling(Options, Vars)-1, Options),
|
|
1878
1878
|
fd_must_be_list(Vars, labeling(Options, Vars)-2),
|
|
1879
1879
|
maplist(finite_domain(labeling(Options, Vars), 2), Vars),
|
|
1880
|
+
eyeprolog__clpz_labeling(Options, Vars).
|
|
1881
|
+
|
|
1882
|
+
labeling_portable(Options, Vars) :-
|
|
1880
1883
|
label(Options, Options, default(leftmost), default(up), default(step), [], upto_ground, Vars).
|
|
1881
1884
|
|
|
1882
1885
|
|
|
@@ -6319,7 +6322,9 @@ with_local_attributes(Vars, Goal, Result) :-
|
|
|
6319
6322
|
local_attributes(Vars-Result),
|
|
6320
6323
|
true).
|
|
6321
6324
|
|
|
6322
|
-
distinct(Vars) -->
|
|
6325
|
+
distinct(Vars) --> eyeprolog__clpz_distinct(Vars).
|
|
6326
|
+
|
|
6327
|
+
distinct_portable(Vars) -->
|
|
6323
6328
|
{ with_local_attributes(Vars,
|
|
6324
6329
|
( difference_arcs(Vars, FreeLeft, FreeRight0),
|
|
6325
6330
|
length(FreeLeft, LFL),
|
package/src/solver.js
CHANGED
package/src/standard-library.js
CHANGED
|
@@ -9,6 +9,8 @@ import { cryptoHostBuiltins } from './crypto-host.js';
|
|
|
9
9
|
import { dcgsHostBuiltins } from './dcgs-host.js';
|
|
10
10
|
import { arithmeticHostBuiltins } from './arithmetic-host.js';
|
|
11
11
|
import { charsioHostBuiltins } from './charsio-host.js';
|
|
12
|
+
import { clpbHostBuiltins } from './clpb-host.js';
|
|
13
|
+
import { clpzHostBuiltins } from './clpz-host.js';
|
|
12
14
|
import { filesHostBuiltins } from './files-host.js';
|
|
13
15
|
import { difHostBuiltins } from './dif-host.js';
|
|
14
16
|
import { formatHostBuiltins } from './format-host.js';
|
|
@@ -378,6 +380,8 @@ export function createEyePrologRegistry() {
|
|
|
378
380
|
expansionBuiltins.register(registry);
|
|
379
381
|
arithmeticHostBuiltins.register(registry);
|
|
380
382
|
charsioHostBuiltins.register(registry);
|
|
383
|
+
clpbHostBuiltins.register(registry);
|
|
384
|
+
clpzHostBuiltins.register(registry);
|
|
381
385
|
filesHostBuiltins.register(registry);
|
|
382
386
|
difHostBuiltins.register(registry);
|
|
383
387
|
formatHostBuiltins.register(registry);
|
|
@@ -465,6 +465,7 @@ answer(X,Y,U,R,F,W,G,C,T) :-
|
|
|
465
465
|
assertEqual(result.stdout,
|
|
466
466
|
'answer(1, 0, "abc", "abc", "aa", yes, shared1, "A", [[1, 3], [2, 4]]).\n',
|
|
467
467
|
'composed common-library answer');
|
|
468
|
+
assertEqual(result.stats.clpb_native_labelings, 1, 'CLP(B) labeling takes its host search-plan path');
|
|
468
469
|
},
|
|
469
470
|
},
|
|
470
471
|
{
|
|
@@ -1303,7 +1304,7 @@ answer(ok) :-
|
|
|
1303
1304
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
1304
1305
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
1305
1306
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
1306
|
-
assertEqual(library.defs.size,
|
|
1307
|
+
assertEqual(library.defs.size, 227, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS predicates, and generic library adapters');
|
|
1307
1308
|
assertEqual(registry.get('eyeprolog__dynify', 1), null, 'Eyelet dynify adapter is absent from the ISO registry');
|
|
1308
1309
|
assertEqual(Boolean(library.get('eyeprolog__dynify', 1)), true, 'Eyelet dynify adapter is an internal EyeProlog library primitive');
|
|
1309
1310
|
assertEqual(registry.get('eyeprolog__eyelet_emit', 2), null, 'Eyelet event adapter is absent from the ISO registry');
|
|
@@ -1359,7 +1360,9 @@ answer(ok) :-
|
|
|
1359
1360
|
assertEqual(registry.get('eyeprolog__countall', 2), null, 'private countall adapter is absent from ISO registry');
|
|
1360
1361
|
assertEqual(Boolean(library.get('eyeprolog__countall', 2)), true, 'private countall adapter is registered for EyeProlog');
|
|
1361
1362
|
assertEqual(Boolean(library.get('eyeprolog__time', 1)), true, 'private time adapter is registered for EyeProlog');
|
|
1362
|
-
assertEqual(library.get('eyeprolog__clpz_labeling', 2),
|
|
1363
|
+
assertEqual(Boolean(library.get('eyeprolog__clpz_labeling', 2)), true, 'CLP(Z) labeling dispatch has a private host adapter');
|
|
1364
|
+
assertEqual(Boolean(library.get('eyeprolog__clpz_distinct', 3)), true, 'CLP(Z) all-distinct propagation has a private host adapter');
|
|
1365
|
+
assertEqual(Boolean(library.get('eyeprolog__clpb_labeling', 1)), true, 'CLP(B) labeling has a private host adapter');
|
|
1363
1366
|
assertEqual(library.get('eyeprolog__clpz_global_cardinality', 3), null, 'CLP(Z) cardinality is implemented in Prolog');
|
|
1364
1367
|
assertEqual(registry.get('put_atts', 2), null, 'put_atts/2 is absent from the ISO registry');
|
|
1365
1368
|
assertEqual(Boolean(library.get('put_atts', 2)), true, 'put_atts/2 is registered for attributed-variable libraries');
|
|
@@ -1586,6 +1589,24 @@ repeated(X) :- X in 1..3, all_distinct([X, X]).
|
|
|
1586
1589
|
'answer(1, 4, 1).\nanswer(2, 3, 0).\npruned(1 \\/ 3).\nhall(3..3).\n', 'CLP(Z) constrained answers');
|
|
1587
1590
|
},
|
|
1588
1591
|
},
|
|
1592
|
+
{
|
|
1593
|
+
name: 'CLP(Z) specializes finite all_distinct first-fail labeling without changing answer order',
|
|
1594
|
+
run: () => {
|
|
1595
|
+
const result = run(`:- use_module(library(clpz)).
|
|
1596
|
+
answer(A, B, C) :-
|
|
1597
|
+
[A, B, C] ins 1..3,
|
|
1598
|
+
all_distinct([A, B, C]),
|
|
1599
|
+
labeling([ff], [A, B, C]).
|
|
1600
|
+
`, { goal: 'answer(A, B, C)' });
|
|
1601
|
+
assertEqual(result.stdout,
|
|
1602
|
+
'answer(1, 2, 3).\nanswer(1, 3, 2).\nanswer(2, 1, 3).\n' +
|
|
1603
|
+
'answer(2, 3, 1).\nanswer(3, 1, 2).\nanswer(3, 2, 1).\n',
|
|
1604
|
+
'native finite-domain search preserves step/up answer order');
|
|
1605
|
+
assertEqual(result.stats.clpz_native_labelings, 1, 'finite all_distinct labeling takes native path');
|
|
1606
|
+
assertEqual(result.stats.clpz_native_distinct_propagations > 0, true,
|
|
1607
|
+
'finite all_distinct propagation takes native matching path');
|
|
1608
|
+
},
|
|
1609
|
+
},
|
|
1589
1610
|
{
|
|
1590
1611
|
name: 'CLP(Z) finite global constraints match the portable example',
|
|
1591
1612
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -9619,7 +9619,7 @@ eyeprolog --proof --goal 'type(X, Y)' examples/socrates.pl
|
|
|
9619
9619
|
eyeprolog --proof examples/socrates.pl > socrates.why.pl
|
|
9620
9620
|
eyeprolog --verify-proof socrates.why.pl examples/socrates.pl
|
|
9621
9621
|
eyeprolog --warnings --goal 'answer(X)' test/conformance/warnings/negation/unstratified_mutual.pl
|
|
9622
|
-
eyeprolog --portable --goal '
|
|
9622
|
+
eyeprolog --portable --goal 'sudoku9(S)' examples/clpz-sudoku-9x9.pl
|
|
9623
9623
|
eyeprolog --stats --goal 'path(a, X)' examples/path-discovery.pl > answers.pl 2> run.stats
|
|
9624
9624
|
```
|
|
9625
9625
|
|
|
@@ -9986,7 +9986,7 @@ mode at a time.
|
|
|
9986
9986
|
| [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) |
|
|
9987
9987
|
| [CLP(Z) N-queens](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-n-queens.pl) | A checked eight-queens witness using finite domains, delayed diagonal constraints, `all_distinct/1`, and first-fail labeling, plus a four-queens multi-solution search. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-n-queens.pl) |
|
|
9988
9988
|
| [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) |
|
|
9989
|
-
| [CLP(Z) Sudoku 9×9](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-sudoku-9x9.pl) | The AI Escargot 9×9 model with finite domains, 27 all-distinct constraints, and first-fail labeling
|
|
9989
|
+
| [CLP(Z) Sudoku 9×9](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-sudoku-9x9.pl) | The AI Escargot 9×9 model with finite domains, 27 all-distinct constraints, and first-fail labeling, genuinely searched from its clues as the default example goal. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-sudoku-9x9.pl) |
|
|
9990
9990
|
| [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) |
|
|
9991
9991
|
| [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) |
|
|
9992
9992
|
| [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) |
|