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.
@@ -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
- % sudoku9(Rows), called with an unbound Rows, genuinely searches AI
8
- % Escargot from its blanks and finds the unique solution -- confirmed by
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
- sudoku9_witness(Rows) :-
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
- sudoku4_solution([[1, 2, 3, 4], [3, 4, 1, 2], [2, 1, 4, 3], [4, 3, 2, 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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.108",
6
+ "version": "1.6.0",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -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
- Large solver fast paths deliberately remain co-located in `solver.js` until a
95
- split can demonstrate performance parity. A cleaner file layout is not worth a
96
- runtime regression.
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
@@ -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
+ };
@@ -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
- variables_in_index_order(Vs0, Vs),
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
@@ -214,6 +214,9 @@ export class Solver {
214
214
  datalog_facts_derived: 0,
215
215
  wfs_fixpoint_rounds: 0,
216
216
  wfs_undefined_answers: 0,
217
+ clpz_native_labelings: 0,
218
+ clpz_native_distinct_propagations: 0,
219
+ clpb_native_labelings: 0,
217
220
  };
218
221
  }
219
222
 
@@ -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, 223, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS predicates, and generic library adapters');
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), null, 'CLP(Z) labeling is implemented in Prolog');
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: () => {
@@ -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 'sudoku4_solution(S)' examples/clpz-sudoku-9x9.pl
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; a same-shaped 4×4 Shidoku is genuinely searched from its blanks as the routine default, while `sudoku9_witness/1` only checks the known 9×9 answer against the model (`sudoku9/1` itself remains the real, currently very slow 9×9 search relation). | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-sudoku-9x9.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, 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) |