eyeprolog 1.1.17 → 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/README.md +1 -1
- package/examples/clpz-global-constraints.pl +35 -0
- package/examples/clpz-sudoku-9x9.pl +64 -0
- package/examples/output/clpz-global-constraints.pl +7 -0
- package/examples/output/clpz-sudoku-9x9.pl +1 -0
- package/package.json +1 -1
- package/playground.html +3 -1
- package/src/clpz.js +498 -18
- package/src/lib/clpz.pl +21 -0
- package/src/playground-worker.js +1 -1
- package/src/standard-library.js +6 -4
- package/test/run-regression.mjs +23 -6
- package/the-art-of-eyeprolog.md +16 -11
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ noun --> [world] | [prolog].
|
|
|
91
91
|
%% goal: phrase(sentence, Words)
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
EyeProlog also adds
|
|
94
|
+
EyeProlog also adds 99 public library predicate indicators to its 129-entry ISO
|
|
95
95
|
profile. **60 are implemented entirely as ordinary Prolog clauses** in focused
|
|
96
96
|
modules under `src/lib/`; the control predicates and finite-domain
|
|
97
97
|
`library(clpz)` kernel use backtrackable host support.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
:- use_module(library(clpz)).
|
|
2
|
+
|
|
3
|
+
% Finite global constraints cover compatibility tables, lexicographic and
|
|
4
|
+
% non-overlapping schedules, value cardinalities with costs, Hamiltonian
|
|
5
|
+
% circuits, distinct-value counting, and three-way integer comparison.
|
|
6
|
+
|
|
7
|
+
%% goal: advanced_clpz(X0, X1)
|
|
8
|
+
|
|
9
|
+
advanced_clpz(table, compatible(4, Y, Distinct)) :-
|
|
10
|
+
tuples_in([[X, Y]], [[1, 2], [1, 5], [4, 0], [4, 3]]),
|
|
11
|
+
X = 4,
|
|
12
|
+
nvalue(Distinct, [X, Y]),
|
|
13
|
+
labeling([], [Y]).
|
|
14
|
+
|
|
15
|
+
advanced_clpz(schedule, starts([A, B, C])) :-
|
|
16
|
+
[A, B, C] ins 0..3,
|
|
17
|
+
lex_chain([[A, B], [B, C]]),
|
|
18
|
+
serialized([A, B, C], [1, 2, 1]),
|
|
19
|
+
A #< B,
|
|
20
|
+
B #< C,
|
|
21
|
+
labeling([ff], [A, B, C]).
|
|
22
|
+
|
|
23
|
+
advanced_clpz(cardinality, assignment(Values, Cost, Order)) :-
|
|
24
|
+
Values = [_, _, _],
|
|
25
|
+
global_cardinality(Values, [1-2, 3-1],
|
|
26
|
+
[cost(Cost, [[5, 1], [2, 4], [3, 6]])]),
|
|
27
|
+
labeling([ff], Values),
|
|
28
|
+
zcompare(Order, Cost, 10).
|
|
29
|
+
|
|
30
|
+
advanced_clpz(circuit, successors(Values)) :-
|
|
31
|
+
Values = [A, B, _, _],
|
|
32
|
+
circuit(Values),
|
|
33
|
+
A #= 2,
|
|
34
|
+
B #= 3,
|
|
35
|
+
labeling([ff], Values).
|
|
@@ -0,0 +1,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,7 @@
|
|
|
1
|
+
advanced_clpz(table, compatible(4, 0, 2)).
|
|
2
|
+
advanced_clpz(table, compatible(4, 3, 2)).
|
|
3
|
+
advanced_clpz(schedule, starts([0, 1, 3])).
|
|
4
|
+
advanced_clpz(cardinality, assignment([1, 1, 3], 13, >)).
|
|
5
|
+
advanced_clpz(cardinality, assignment([1, 3, 1], 12, >)).
|
|
6
|
+
advanced_clpz(cardinality, assignment([3, 1, 1], 6, <)).
|
|
7
|
+
advanced_clpz(circuit, successors([2, 3, 4, 1])).
|
|
@@ -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
|
@@ -465,8 +465,10 @@
|
|
|
465
465
|
"chart-parser",
|
|
466
466
|
"clinical-trial-screening",
|
|
467
467
|
"clpz-factorial",
|
|
468
|
+
"clpz-global-constraints",
|
|
468
469
|
"clpz-n-queens",
|
|
469
470
|
"clpz-resource-allocation",
|
|
471
|
+
"clpz-sudoku-9x9",
|
|
470
472
|
"collatz-1000",
|
|
471
473
|
"combinatorics-findall-sort",
|
|
472
474
|
"competitive-enzyme-kinetics",
|
|
@@ -961,7 +963,7 @@
|
|
|
961
963
|
runButton.disabled = true;
|
|
962
964
|
stopButton.disabled = false;
|
|
963
965
|
|
|
964
|
-
const workerUrl = new URL('./src/playground-worker.js?playground=
|
|
966
|
+
const workerUrl = new URL('./src/playground-worker.js?playground=20260811c', location.href);
|
|
965
967
|
activeWorker = new Worker(workerUrl, { type: 'module' });
|
|
966
968
|
const worker = activeWorker;
|
|
967
969
|
const request = {
|
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) {
|
|
@@ -19,10 +21,17 @@ export const clpzBuiltins = {
|
|
|
19
21
|
registry.add('eyeprolog__clpz_in', 2, inBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
20
22
|
registry.add('eyeprolog__clpz_ins', 2, insBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
21
23
|
registry.add('eyeprolog__clpz_all_distinct', 1, allDistinctBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
24
|
+
registry.add('eyeprolog__clpz_nvalue', 2, nvalueBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
22
25
|
registry.add('eyeprolog__clpz_sum', 3, sumBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
23
26
|
registry.add('eyeprolog__clpz_scalar_product', 4, scalarProductBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
27
|
+
registry.add('eyeprolog__clpz_tuples_in', 2, tuplesInBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
28
|
+
registry.add('eyeprolog__clpz_lex_chain', 1, lexChainBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
29
|
+
registry.add('eyeprolog__clpz_serialized', 2, serializedBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
30
|
+
registry.add('eyeprolog__clpz_global_cardinality', 3, globalCardinalityBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
31
|
+
registry.add('eyeprolog__clpz_circuit', 1, circuitBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
24
32
|
registry.add('eyeprolog__clpz_chain', 2, chainBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
25
33
|
registry.add('eyeprolog__clpz_element', 3, elementBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
34
|
+
registry.add('eyeprolog__clpz_zcompare', 3, zcompareBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
26
35
|
registry.add('eyeprolog__clpz_labeling', 2, labelingBuiltin, { eyePrologLibrary: true });
|
|
27
36
|
registry.add('eyeprolog__clpz_fd_var', 1, fdVarBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
28
37
|
registry.add('eyeprolog__clpz_fd_inf', 2, fdBoundBuiltin('inf'), { deterministic: true, eyePrologLibrary: true });
|
|
@@ -186,40 +195,60 @@ function rootVariableName(term, env) {
|
|
|
186
195
|
return term.type === VAR ? term.name : null;
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
function
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
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));
|
|
193
208
|
}
|
|
194
|
-
|
|
209
|
+
domainCacheByState.set(env._state, { domains: store.domains, roots });
|
|
210
|
+
return roots;
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
function intersectValues(left, right) {
|
|
198
|
-
const allowed = new Set(right
|
|
199
|
-
return left.filter((value) => allowed.has(
|
|
214
|
+
const allowed = new Set(right);
|
|
215
|
+
return left.filter((value) => allowed.has(value));
|
|
200
216
|
}
|
|
201
217
|
|
|
202
218
|
function domainForRoot(root, env) {
|
|
203
|
-
|
|
204
|
-
if (domains.length === 0) return null;
|
|
205
|
-
return domains.slice(1).reduce(intersectValues, domains[0]);
|
|
219
|
+
return domainsByRoot(env).get(root) ?? null;
|
|
206
220
|
}
|
|
207
221
|
|
|
208
222
|
function constrainTermDomain(next, term, values) {
|
|
223
|
+
const result = narrowTermDomain(next, term, values);
|
|
224
|
+
return result.ok && clpzStateConsistent(next);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function narrowTermDomain(next, term, values) {
|
|
209
228
|
const resolved = deref(term, next);
|
|
210
229
|
if (resolved.type === NUMBER) {
|
|
211
230
|
const value = integerValue(resolved, next);
|
|
212
|
-
return values.some((candidate) => candidate === value);
|
|
231
|
+
return { ok: values.some((candidate) => candidate === value), changed: false };
|
|
213
232
|
}
|
|
214
233
|
if (resolved.type !== VAR) throw new PrologError('type_error(integer)', resolved);
|
|
215
234
|
const existing = domainForRoot(resolved.name, next);
|
|
216
235
|
const narrowed = existing == null ? values : intersectValues(existing, values);
|
|
217
|
-
if (narrowed.length === 0) return false;
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
236
|
+
if (narrowed.length === 0) return { ok: false, changed: false };
|
|
237
|
+
const changed = existing == null || existing.length !== narrowed.length ||
|
|
238
|
+
existing.some((value, index) => value !== narrowed[index]);
|
|
239
|
+
if (!changed) return { ok: true, changed: false };
|
|
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
|
+
}
|
|
248
|
+
if (narrowed.length === 1 && !unify(resolved, numberTerm(narrowed[0].toString()), next)) {
|
|
249
|
+
return { ok: false, changed: false };
|
|
250
|
+
}
|
|
251
|
+
return { ok: true, changed: true };
|
|
223
252
|
}
|
|
224
253
|
|
|
225
254
|
function* inBuiltin({ goal, env }) {
|
|
@@ -248,11 +277,36 @@ function listArgument(term, env) {
|
|
|
248
277
|
return items;
|
|
249
278
|
}
|
|
250
279
|
|
|
280
|
+
function nestedListArgument(term, env) {
|
|
281
|
+
return listArgument(term, env).map((item) => listArgument(item, env));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function integerListArgument(term, env) {
|
|
285
|
+
return listArgument(term, env).map((item) => {
|
|
286
|
+
const value = integerValue(item, env);
|
|
287
|
+
if (value == null) throw new PrologError('instantiation_error');
|
|
288
|
+
return value;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function integerRange(lower, upper) {
|
|
293
|
+
const values = [];
|
|
294
|
+
for (let value = BigInt(lower); value <= BigInt(upper); value++) values.push(value);
|
|
295
|
+
return values;
|
|
296
|
+
}
|
|
297
|
+
|
|
251
298
|
function* allDistinctBuiltin({ goal, env }) {
|
|
252
299
|
const next = env.clone();
|
|
253
300
|
if (addGlobal(next, { kind: 'allDistinct', terms: listArgument(goal.args[0], env) })) yield next;
|
|
254
301
|
}
|
|
255
302
|
|
|
303
|
+
function* nvalueBuiltin({ goal, env }) {
|
|
304
|
+
const terms = listArgument(goal.args[1], env);
|
|
305
|
+
const next = env.clone();
|
|
306
|
+
if (!constrainTermDomain(next, goal.args[0], integerRange(terms.length === 0 ? 0 : 1, terms.length))) return;
|
|
307
|
+
if (addGlobal(next, { kind: 'nvalue', count: goal.args[0], terms })) yield next;
|
|
308
|
+
}
|
|
309
|
+
|
|
256
310
|
function* sumBuiltin({ goal, env }) {
|
|
257
311
|
const next = env.clone();
|
|
258
312
|
const relation = relationName(goal.args[1], env);
|
|
@@ -269,6 +323,93 @@ function* scalarProductBuiltin({ goal, env }) {
|
|
|
269
323
|
if (addGlobal(next, { kind: 'scalarProduct', coefficients, terms, relation, value: goal.args[3] })) yield next;
|
|
270
324
|
}
|
|
271
325
|
|
|
326
|
+
function* tuplesInBuiltin({ goal, env }) {
|
|
327
|
+
const tuples = nestedListArgument(goal.args[0], env);
|
|
328
|
+
const relation = nestedListArgument(goal.args[1], env).map((row) => row.map((item) => {
|
|
329
|
+
const value = integerValue(item, env);
|
|
330
|
+
if (value == null) throw new PrologError('instantiation_error');
|
|
331
|
+
return value;
|
|
332
|
+
}));
|
|
333
|
+
const next = env.clone();
|
|
334
|
+
if (addGlobal(next, { kind: 'tuplesIn', tuples, relation })) yield next;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function* lexChainBuiltin({ goal, env }) {
|
|
338
|
+
const lists = nestedListArgument(goal.args[0], env);
|
|
339
|
+
if (lists.some((list) => list.length !== (lists[0]?.length ?? 0))) return;
|
|
340
|
+
const next = env.clone();
|
|
341
|
+
if (addGlobal(next, { kind: 'lexChain', lists })) yield next;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function* serializedBuiltin({ goal, env }) {
|
|
345
|
+
const starts = listArgument(goal.args[0], env);
|
|
346
|
+
const durations = integerListArgument(goal.args[1], env);
|
|
347
|
+
if (starts.length !== durations.length) throw new PrologError('domain_error(same_length)');
|
|
348
|
+
if (durations.some((duration) => duration < 0n)) throw new PrologError('domain_error(not_less_than_zero)');
|
|
349
|
+
const next = env.clone();
|
|
350
|
+
if (addGlobal(next, { kind: 'serialized', starts, durations })) yield next;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function gccPairs(term, env) {
|
|
354
|
+
const pairs = listArgument(term, env).map((pair) => {
|
|
355
|
+
const resolved = deref(pair, env);
|
|
356
|
+
if (resolved.type !== COMPOUND || resolved.name !== '-' || resolved.arity !== 2) {
|
|
357
|
+
throw new PrologError('domain_error(gcc_pair)', resolved);
|
|
358
|
+
}
|
|
359
|
+
const key = integerValue(resolved.args[0], env);
|
|
360
|
+
if (key == null) throw new PrologError('instantiation_error');
|
|
361
|
+
return { key, count: resolved.args[1] };
|
|
362
|
+
});
|
|
363
|
+
const keys = new Set(pairs.map(({ key }) => key.toString()));
|
|
364
|
+
if (keys.size !== pairs.length) throw new PrologError('domain_error(gcc_unique_key_pairs)', deref(term, env));
|
|
365
|
+
return pairs;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function gccOptions(term, env, rowCount, columnCount) {
|
|
369
|
+
let cost = null;
|
|
370
|
+
let matrix = null;
|
|
371
|
+
for (const option of listArgument(term, env)) {
|
|
372
|
+
const resolved = deref(option, env);
|
|
373
|
+
if (resolved.type === COMPOUND && resolved.name === 'consistency' && resolved.arity === 1 &&
|
|
374
|
+
deref(resolved.args[0], env).type === ATOM && deref(resolved.args[0], env).name === 'value') continue;
|
|
375
|
+
if (resolved.type === COMPOUND && resolved.name === 'cost' && resolved.arity === 2 && cost == null) {
|
|
376
|
+
cost = resolved.args[0];
|
|
377
|
+
matrix = nestedListArgument(resolved.args[1], env).map((row) => row.map((item) => {
|
|
378
|
+
const value = integerValue(item, env);
|
|
379
|
+
if (value == null) throw new PrologError('instantiation_error');
|
|
380
|
+
return value;
|
|
381
|
+
}));
|
|
382
|
+
if (matrix.length !== rowCount || matrix.some((row) => row.length !== columnCount)) {
|
|
383
|
+
throw new PrologError('domain_error(gcc_cost_matrix)', resolved.args[1]);
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
throw new PrologError('domain_error(global_cardinality_option)', resolved);
|
|
388
|
+
}
|
|
389
|
+
return { cost, matrix };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function* globalCardinalityBuiltin({ goal, env }) {
|
|
393
|
+
const terms = listArgument(goal.args[0], env);
|
|
394
|
+
const pairs = gccPairs(goal.args[1], env);
|
|
395
|
+
const options = gccOptions(goal.args[2], env, terms.length, pairs.length);
|
|
396
|
+
const keys = pairs.map(({ key }) => key).sort(compareBigInt);
|
|
397
|
+
const next = env.clone();
|
|
398
|
+
for (const term of terms) if (!constrainTermDomain(next, term, keys)) return;
|
|
399
|
+
for (const { count } of pairs) if (!constrainTermDomain(next, count, integerRange(0, terms.length))) return;
|
|
400
|
+
if (addGlobal(next, { kind: 'globalCardinality', terms, pairs, ...options })) yield next;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function* circuitBuiltin({ goal, env }) {
|
|
404
|
+
const terms = listArgument(goal.args[0], env);
|
|
405
|
+
const next = env.clone();
|
|
406
|
+
if (terms.length > 0) {
|
|
407
|
+
const values = integerRange(1, terms.length);
|
|
408
|
+
for (const term of terms) if (!constrainTermDomain(next, term, values)) return;
|
|
409
|
+
}
|
|
410
|
+
if (addGlobal(next, { kind: 'circuit', terms })) yield next;
|
|
411
|
+
}
|
|
412
|
+
|
|
272
413
|
function* chainBuiltin({ goal, env }) {
|
|
273
414
|
const terms = listArgument(goal.args[1], env);
|
|
274
415
|
const relation = relationName(goal.args[0], env);
|
|
@@ -282,6 +423,70 @@ function* elementBuiltin({ goal, env }) {
|
|
|
282
423
|
if (addGlobal(next, { kind: 'element', index: goal.args[0], terms: listArgument(goal.args[1], env), value: goal.args[2] })) yield next;
|
|
283
424
|
}
|
|
284
425
|
|
|
426
|
+
function* zcompareBuiltin({ goal, env }) {
|
|
427
|
+
const next = env.clone();
|
|
428
|
+
if (addGlobal(next, { kind: 'zcompare', order: goal.args[0], left: goal.args[1], right: goal.args[2] })) yield next;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function tupleCandidates(tuple, relation, env) {
|
|
432
|
+
return relation.filter((row) => row.length === tuple.length && row.every((candidate, index) => {
|
|
433
|
+
const resolved = deref(tuple[index], env);
|
|
434
|
+
if (resolved.type === NUMBER) return integerValue(resolved, env) === candidate;
|
|
435
|
+
if (resolved.type !== VAR) throw new PrologError('type_error(integer)', resolved);
|
|
436
|
+
const domain = domainForRoot(resolved.name, env);
|
|
437
|
+
return domain == null || domain.some((value) => value === candidate);
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function lexPairTruth(left, right, env) {
|
|
442
|
+
if (left.length !== right.length) return false;
|
|
443
|
+
for (let index = 0; index < left.length; index++) {
|
|
444
|
+
const a = expressionValue(left[index], env);
|
|
445
|
+
const b = expressionValue(right[index], env);
|
|
446
|
+
if (a == null || b == null) return true;
|
|
447
|
+
if (a < b) return true;
|
|
448
|
+
if (a > b) return false;
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function circuitTruth(terms, env) {
|
|
454
|
+
if (terms.length === 0) return true;
|
|
455
|
+
const values = terms.map((term) => expressionValue(term, env));
|
|
456
|
+
const seenValues = new Set();
|
|
457
|
+
for (let index = 0; index < values.length; index++) {
|
|
458
|
+
const value = values[index];
|
|
459
|
+
if (value == null) continue;
|
|
460
|
+
if (value < 1n || value > BigInt(values.length)) return false;
|
|
461
|
+
if (values.length > 1 && value === BigInt(index + 1)) return false;
|
|
462
|
+
const key = value.toString();
|
|
463
|
+
if (seenValues.has(key)) return false;
|
|
464
|
+
seenValues.add(key);
|
|
465
|
+
}
|
|
466
|
+
if (values.some((value) => value == null)) return true;
|
|
467
|
+
const visited = new Set();
|
|
468
|
+
let node = 1;
|
|
469
|
+
for (let step = 0; step < values.length; step++) {
|
|
470
|
+
if (visited.has(node)) return false;
|
|
471
|
+
visited.add(node);
|
|
472
|
+
node = Number(values[node - 1]);
|
|
473
|
+
}
|
|
474
|
+
return node === 1 && visited.size === values.length;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function orderAtom(term, env) {
|
|
478
|
+
const resolved = deref(term, env);
|
|
479
|
+
if (resolved.type === VAR) return null;
|
|
480
|
+
if (resolved.type !== ATOM || !['<', '=', '>'].includes(resolved.name)) {
|
|
481
|
+
throw new PrologError('domain_error(order)', resolved);
|
|
482
|
+
}
|
|
483
|
+
return resolved.name;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function comparedOrder(left, right) {
|
|
487
|
+
return left < right ? '<' : left > right ? '>' : '=';
|
|
488
|
+
}
|
|
489
|
+
|
|
285
490
|
function globalTruth(global, env) {
|
|
286
491
|
if (global.kind === 'allDistinct') {
|
|
287
492
|
const seen = new Set();
|
|
@@ -294,6 +499,14 @@ function globalTruth(global, env) {
|
|
|
294
499
|
}
|
|
295
500
|
return true;
|
|
296
501
|
}
|
|
502
|
+
if (global.kind === 'nvalue') {
|
|
503
|
+
const values = global.terms.map((term) => expressionValue(term, env));
|
|
504
|
+
const known = new Set(values.filter((value) => value != null).map(String));
|
|
505
|
+
const count = expressionValue(global.count, env);
|
|
506
|
+
if (count == null) return true;
|
|
507
|
+
if (count < BigInt(known.size) || count > BigInt(known.size + values.filter((value) => value == null).length)) return false;
|
|
508
|
+
return values.some((value) => value == null) || count === BigInt(known.size);
|
|
509
|
+
}
|
|
297
510
|
if (global.kind === 'sum') {
|
|
298
511
|
const values = global.terms.map((term) => expressionValue(term, env));
|
|
299
512
|
const right = expressionValue(global.value, env);
|
|
@@ -309,6 +522,54 @@ function globalTruth(global, env) {
|
|
|
309
522
|
for (let i = 0; i < values.length; i++) total += integerValue(global.coefficients[i], env) * values[i];
|
|
310
523
|
return relationTruth(relationTerm(global.relation, numberTerm(total.toString()), global.value), env) !== false;
|
|
311
524
|
}
|
|
525
|
+
if (global.kind === 'tuplesIn') {
|
|
526
|
+
return global.tuples.every((tuple) => tupleCandidates(tuple, global.relation, env).length > 0);
|
|
527
|
+
}
|
|
528
|
+
if (global.kind === 'lexChain') {
|
|
529
|
+
for (let index = 1; index < global.lists.length; index++) {
|
|
530
|
+
if (!lexPairTruth(global.lists[index - 1], global.lists[index], env)) return false;
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
if (global.kind === 'serialized') {
|
|
535
|
+
for (let left = 0; left < global.starts.length; left++) {
|
|
536
|
+
const a = expressionValue(global.starts[left], env);
|
|
537
|
+
if (a == null) continue;
|
|
538
|
+
for (let right = left + 1; right < global.starts.length; right++) {
|
|
539
|
+
const b = expressionValue(global.starts[right], env);
|
|
540
|
+
if (b == null) continue;
|
|
541
|
+
if (a + global.durations[left] > b && b + global.durations[right] > a) return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (global.kind === 'globalCardinality') {
|
|
547
|
+
const values = global.terms.map((term) => expressionValue(term, env));
|
|
548
|
+
for (const { key, count: countTerm } of global.pairs) {
|
|
549
|
+
const known = values.filter((value) => value === key).length;
|
|
550
|
+
const possible = global.terms.filter((term, index) => {
|
|
551
|
+
if (values[index] != null) return false;
|
|
552
|
+
const resolved = deref(term, env);
|
|
553
|
+
const domain = resolved.type === VAR ? domainForRoot(resolved.name, env) : null;
|
|
554
|
+
return domain == null || domain.some((value) => value === key);
|
|
555
|
+
}).length;
|
|
556
|
+
const count = expressionValue(countTerm, env);
|
|
557
|
+
if (count != null && (count < BigInt(known) || count > BigInt(known + possible))) return false;
|
|
558
|
+
if (possible === 0 && count != null && count !== BigInt(known)) return false;
|
|
559
|
+
}
|
|
560
|
+
if (global.cost != null && values.every((value) => value != null)) {
|
|
561
|
+
let total = 0n;
|
|
562
|
+
for (let row = 0; row < values.length; row++) {
|
|
563
|
+
const column = global.pairs.findIndex(({ key }) => key === values[row]);
|
|
564
|
+
if (column < 0) return false;
|
|
565
|
+
total += global.matrix[row][column];
|
|
566
|
+
}
|
|
567
|
+
const cost = expressionValue(global.cost, env);
|
|
568
|
+
if (cost != null && cost !== total) return false;
|
|
569
|
+
}
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
if (global.kind === 'circuit') return circuitTruth(global.terms, env);
|
|
312
573
|
if (global.kind === 'chain') {
|
|
313
574
|
for (let i = 1; i < global.terms.length; i++) {
|
|
314
575
|
const truth = relationTruth(relationTerm(global.relation, global.terms[i - 1], global.terms[i]), env);
|
|
@@ -325,6 +586,12 @@ function globalTruth(global, env) {
|
|
|
325
586
|
const right = expressionValue(global.value, env);
|
|
326
587
|
return left == null || right == null || left === right;
|
|
327
588
|
}
|
|
589
|
+
if (global.kind === 'zcompare') {
|
|
590
|
+
const order = orderAtom(global.order, env);
|
|
591
|
+
const left = expressionValue(global.left, env);
|
|
592
|
+
const right = expressionValue(global.right, env);
|
|
593
|
+
return order == null || left == null || right == null || order === comparedOrder(left, right);
|
|
594
|
+
}
|
|
328
595
|
return true;
|
|
329
596
|
}
|
|
330
597
|
|
|
@@ -395,9 +662,143 @@ function bindLinearEquality(left, right, env) {
|
|
|
395
662
|
return { ok: unify(variable(name), numberTerm(value.toString()), env), changed: true };
|
|
396
663
|
}
|
|
397
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
|
+
|
|
398
786
|
function propagate(env) {
|
|
399
787
|
const store = env._clpz;
|
|
400
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;
|
|
401
802
|
let changed;
|
|
402
803
|
do {
|
|
403
804
|
changed = false;
|
|
@@ -410,7 +811,86 @@ function propagate(env) {
|
|
|
410
811
|
}
|
|
411
812
|
}
|
|
412
813
|
for (const global of store.globals) {
|
|
413
|
-
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') {
|
|
819
|
+
const values = global.terms.map((term) => expressionValue(term, env));
|
|
820
|
+
const known = new Set(values.filter((value) => value != null).map(String));
|
|
821
|
+
const unresolved = values.filter((value) => value == null).length;
|
|
822
|
+
const result = narrowTermDomain(env, global.count, integerRange(known.size, known.size + unresolved));
|
|
823
|
+
if (!result.ok) return false;
|
|
824
|
+
changed ||= result.changed;
|
|
825
|
+
} else if (global.kind === 'tuplesIn') {
|
|
826
|
+
for (const tuple of global.tuples) {
|
|
827
|
+
const candidates = tupleCandidates(tuple, global.relation, env);
|
|
828
|
+
if (candidates.length === 0) return false;
|
|
829
|
+
for (let index = 0; index < tuple.length; index++) {
|
|
830
|
+
const values = [...new Set(candidates.map((row) => row[index].toString()))].map(BigInt).sort(compareBigInt);
|
|
831
|
+
const result = narrowTermDomain(env, tuple[index], values);
|
|
832
|
+
if (!result.ok) return false;
|
|
833
|
+
changed ||= result.changed;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} else if (global.kind === 'globalCardinality') {
|
|
837
|
+
const values = global.terms.map((term) => expressionValue(term, env));
|
|
838
|
+
for (const { key, count: countTerm } of global.pairs) {
|
|
839
|
+
const known = values.filter((value) => value === key).length;
|
|
840
|
+
const possibleTerms = global.terms.filter((term, index) => {
|
|
841
|
+
if (values[index] != null) return false;
|
|
842
|
+
const resolved = deref(term, env);
|
|
843
|
+
const domain = resolved.type === VAR ? domainForRoot(resolved.name, env) : null;
|
|
844
|
+
return domain == null || domain.some((value) => value === key);
|
|
845
|
+
});
|
|
846
|
+
const countResult = narrowTermDomain(env, countTerm, integerRange(known, known + possibleTerms.length));
|
|
847
|
+
if (!countResult.ok) return false;
|
|
848
|
+
changed ||= countResult.changed;
|
|
849
|
+
const count = expressionValue(countTerm, env);
|
|
850
|
+
if (count === BigInt(known)) {
|
|
851
|
+
for (const term of possibleTerms) {
|
|
852
|
+
const resolved = deref(term, env);
|
|
853
|
+
if (resolved.type !== VAR) continue;
|
|
854
|
+
const domain = domainForRoot(resolved.name, env);
|
|
855
|
+
const result = narrowTermDomain(env, term, domain.filter((value) => value !== key));
|
|
856
|
+
if (!result.ok) return false;
|
|
857
|
+
changed ||= result.changed;
|
|
858
|
+
}
|
|
859
|
+
} else if (count === BigInt(known + possibleTerms.length)) {
|
|
860
|
+
for (const term of possibleTerms) {
|
|
861
|
+
const result = narrowTermDomain(env, term, [key]);
|
|
862
|
+
if (!result.ok) return false;
|
|
863
|
+
changed ||= result.changed;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (global.cost != null && values.every((value) => value != null)) {
|
|
868
|
+
let total = 0n;
|
|
869
|
+
for (let row = 0; row < values.length; row++) {
|
|
870
|
+
const column = global.pairs.findIndex(({ key }) => key === values[row]);
|
|
871
|
+
if (column < 0) return false;
|
|
872
|
+
total += global.matrix[row][column];
|
|
873
|
+
}
|
|
874
|
+
const result = bindExpressionEquality(global.cost, numberTerm(total.toString()), env);
|
|
875
|
+
if (!result.ok) return false;
|
|
876
|
+
changed ||= result.changed;
|
|
877
|
+
}
|
|
878
|
+
} else if (global.kind === 'zcompare') {
|
|
879
|
+
const order = orderAtom(global.order, env);
|
|
880
|
+
const left = expressionValue(global.left, env);
|
|
881
|
+
const right = expressionValue(global.right, env);
|
|
882
|
+
if (left != null && right != null) {
|
|
883
|
+
const resolvedOrder = atom(comparedOrder(left, right));
|
|
884
|
+
if (order == null) {
|
|
885
|
+
if (!unify(global.order, resolvedOrder, env)) return false;
|
|
886
|
+
changed = true;
|
|
887
|
+
} else if (order !== resolvedOrder.name) return false;
|
|
888
|
+
} else if (order === '=') {
|
|
889
|
+
const result = bindLinearEquality(global.left, global.right, env);
|
|
890
|
+
if (!result.ok) return false;
|
|
891
|
+
changed ||= result.changed;
|
|
892
|
+
}
|
|
893
|
+
} else if (global.kind === 'element') {
|
|
414
894
|
const index = expressionValue(global.index, env);
|
|
415
895
|
if (index != null) {
|
|
416
896
|
if (index < 1n || index > BigInt(global.terms.length)) return false;
|
package/src/lib/clpz.pl
CHANGED
|
@@ -25,13 +25,21 @@
|
|
|
25
25
|
ins/2,
|
|
26
26
|
all_different/1,
|
|
27
27
|
all_distinct/1,
|
|
28
|
+
nvalue/2,
|
|
28
29
|
sum/3,
|
|
29
30
|
scalar_product/4,
|
|
31
|
+
tuples_in/2,
|
|
30
32
|
labeling/2,
|
|
31
33
|
label/1,
|
|
32
34
|
indomain/1,
|
|
35
|
+
lex_chain/1,
|
|
36
|
+
serialized/2,
|
|
37
|
+
global_cardinality/2,
|
|
38
|
+
global_cardinality/3,
|
|
39
|
+
circuit/1,
|
|
33
40
|
chain/2,
|
|
34
41
|
element/3,
|
|
42
|
+
zcompare/3,
|
|
35
43
|
fd_var/1,
|
|
36
44
|
fd_inf/2,
|
|
37
45
|
fd_sup/2,
|
|
@@ -75,6 +83,7 @@ ins(Integers, Domain) :- eyeprolog__clpz_ins(Integers, Domain).
|
|
|
75
83
|
|
|
76
84
|
all_different(Integers) :- all_distinct(Integers).
|
|
77
85
|
all_distinct(Integers) :- eyeprolog__clpz_all_distinct(Integers).
|
|
86
|
+
nvalue(Count, Integers) :- eyeprolog__clpz_nvalue(Count, Integers).
|
|
78
87
|
|
|
79
88
|
sum(Integers, Relation, Value) :-
|
|
80
89
|
eyeprolog__clpz_sum(Integers, Relation, Value).
|
|
@@ -82,15 +91,27 @@ sum(Integers, Relation, Value) :-
|
|
|
82
91
|
scalar_product(Coefficients, Integers, Relation, Value) :-
|
|
83
92
|
eyeprolog__clpz_scalar_product(Coefficients, Integers, Relation, Value).
|
|
84
93
|
|
|
94
|
+
tuples_in(Tuples, Relation) :- eyeprolog__clpz_tuples_in(Tuples, Relation).
|
|
95
|
+
|
|
85
96
|
labeling(Options, Integers) :-
|
|
86
97
|
eyeprolog__clpz_labeling(Options, Integers).
|
|
87
98
|
|
|
88
99
|
label(Integers) :- labeling([], Integers).
|
|
89
100
|
indomain(Integer) :- labeling([], [Integer]).
|
|
90
101
|
|
|
102
|
+
lex_chain(Lists) :- eyeprolog__clpz_lex_chain(Lists).
|
|
103
|
+
serialized(Starts, Durations) :- eyeprolog__clpz_serialized(Starts, Durations).
|
|
104
|
+
|
|
105
|
+
global_cardinality(Integers, Pairs) :-
|
|
106
|
+
global_cardinality(Integers, Pairs, []).
|
|
107
|
+
global_cardinality(Integers, Pairs, Options) :-
|
|
108
|
+
eyeprolog__clpz_global_cardinality(Integers, Pairs, Options).
|
|
109
|
+
|
|
110
|
+
circuit(Integers) :- eyeprolog__clpz_circuit(Integers).
|
|
91
111
|
chain(Relation, Integers) :- eyeprolog__clpz_chain(Relation, Integers).
|
|
92
112
|
element(Index, Integers, Value) :-
|
|
93
113
|
eyeprolog__clpz_element(Index, Integers, Value).
|
|
114
|
+
zcompare(Order, Left, Right) :- eyeprolog__clpz_zcompare(Order, Left, Right).
|
|
94
115
|
|
|
95
116
|
fd_var(Integer) :- eyeprolog__clpz_fd_var(Integer).
|
|
96
117
|
fd_inf(Integer, Infimum) :- eyeprolog__clpz_fd_inf(Integer, Infimum).
|
package/src/playground-worker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Browser worker entry used by playground.html.
|
|
2
2
|
// Keep this module free of Node-only imports: it is fetched directly by the
|
|
3
3
|
// browser and is also exercised by test/run-playground.mjs.
|
|
4
|
-
import { createEyePrologRegistry, run } from './index.js?playground=
|
|
4
|
+
import { createEyePrologRegistry, run } from './index.js?playground=20260811c';
|
|
5
5
|
|
|
6
6
|
const registry = createEyePrologRegistry();
|
|
7
7
|
|
package/src/standard-library.js
CHANGED
|
@@ -22,7 +22,7 @@ const moduleFiles = Object.freeze({
|
|
|
22
22
|
|
|
23
23
|
const cacheKey = isNode
|
|
24
24
|
? null
|
|
25
|
-
: (new URL(import.meta.url).searchParams.get('playground') ?? '
|
|
25
|
+
: (new URL(import.meta.url).searchParams.get('playground') ?? '20260811c');
|
|
26
26
|
|
|
27
27
|
export const standardLibrarySources = new Map(await Promise.all(
|
|
28
28
|
Object.entries(moduleFiles).map(async ([name, filename]) => [name, {
|
|
@@ -48,9 +48,11 @@ export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
|
48
48
|
'call_nth/2', 'freeze/2',
|
|
49
49
|
'#>/2', '#</2', '#>=/2', '#=</2', '#=/2', '#\\=/2', '#\\/1',
|
|
50
50
|
'#<==>/2', '#==>/2', '#<==/2', '#\\//2', '#\\/2', '#/\\/2',
|
|
51
|
-
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'sum/3',
|
|
52
|
-
'scalar_product/4', '
|
|
53
|
-
'
|
|
51
|
+
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
|
|
52
|
+
'scalar_product/4', 'tuples_in/2', 'labeling/2', 'label/1',
|
|
53
|
+
'indomain/1', 'lex_chain/1', 'serialized/2', 'global_cardinality/2',
|
|
54
|
+
'global_cardinality/3', 'circuit/1', 'chain/2', 'element/3', 'zcompare/3',
|
|
55
|
+
'fd_var/1', 'fd_inf/2', 'fd_sup/2', 'fd_size/2', 'fd_dom/2',
|
|
54
56
|
]);
|
|
55
57
|
export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
56
58
|
'sumall/3', 'aggregate_min/5', 'aggregate_max/5',
|
package/test/run-regression.mjs
CHANGED
|
@@ -1438,20 +1438,21 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
1438
1438
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
1439
1439
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
1440
1440
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
1441
|
-
assertEqual(library.defs.size,
|
|
1441
|
+
assertEqual(library.defs.size, 152, 'EyeProlog registry contains ISO definitions and private library adapters');
|
|
1442
1442
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
1443
1443
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
1444
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
1444
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 39, 'public native EyeProlog builtin count');
|
|
1445
1445
|
assertEqual(eyePrologPortableLibraryIndicators.length, 60, 'portable Prolog library count');
|
|
1446
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
1446
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 39, 'native host library count');
|
|
1447
1447
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
|
|
1448
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
1448
|
+
assertEqual(eyePrologLibraryIndicators.length, 99, 'complete EyeProlog library surface');
|
|
1449
1449
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
1450
1450
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
1451
1451
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
|
1452
1452
|
assertEqual(registry.get('eyeprolog__freeze', 2), null, 'private freeze adapter is absent from ISO registry');
|
|
1453
1453
|
assertEqual(Boolean(library.get('eyeprolog__freeze', 2)), true, 'private freeze adapter is registered for EyeProlog');
|
|
1454
1454
|
assertEqual(Boolean(library.get('eyeprolog__clpz_labeling', 2)), true, 'private CLP(Z) labeling adapter is registered');
|
|
1455
|
+
assertEqual(Boolean(library.get('eyeprolog__clpz_global_cardinality', 3)), true, 'private CLP(Z) cardinality adapter is registered');
|
|
1455
1456
|
assertEqual(library.get('between', 3), null, 'between/3 remains portable Prolog');
|
|
1456
1457
|
assertEqual(library.get('smallest_divisor_from', 3), null, 'smallest_divisor_from/3 remains portable Prolog');
|
|
1457
1458
|
assertEqual(library.get('random', 3), null, 'random/3 remains portable Prolog');
|
|
@@ -1539,10 +1540,26 @@ answer(X, Y, B) :-
|
|
|
1539
1540
|
B #<==> X #= 1,
|
|
1540
1541
|
labeling([ff, down], [X, Y, B]).
|
|
1541
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]).
|
|
1542
1546
|
`);
|
|
1543
1547
|
assertEqual(program.findGroup('labeling', 2)?.module, 'clpz', 'labeling/2 module');
|
|
1544
|
-
assertEqual(run(program, { goals: ['answer(X, Y, B)', 'contradiction'] }).stdout,
|
|
1545
|
-
'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');
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
name: 'CLP(Z) finite global constraints match the portable example',
|
|
1554
|
+
run: () => {
|
|
1555
|
+
const filename = path.join(packageRoot, 'examples', 'clpz-global-constraints.pl');
|
|
1556
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
1557
|
+
const program = Program.parseSources([{ text: source, filename }]);
|
|
1558
|
+
assertEqual(program.findGroup('tuples_in', 2)?.module, 'clpz', 'tuples_in/2 module');
|
|
1559
|
+
assertEqual(program.findGroup('global_cardinality', 3)?.module, 'clpz', 'global_cardinality/3 module');
|
|
1560
|
+
assertEqual(program.findGroup('circuit', 1)?.module, 'clpz', 'circuit/1 module');
|
|
1561
|
+
const expected = fs.readFileSync(path.join(packageRoot, 'examples', 'output', 'clpz-global-constraints.pl'), 'utf8');
|
|
1562
|
+
assertEqual(run(program, { goal: 'advanced_clpz(X0, X1)' }).stdout, expected, 'global constraint answers');
|
|
1546
1563
|
},
|
|
1547
1564
|
},
|
|
1548
1565
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -5762,14 +5762,14 @@ so side effects occur in Prolog execution order.
|
|
|
5762
5762
|
|
|
5763
5763
|
### The EyeProlog library
|
|
5764
5764
|
|
|
5765
|
-
EyeProlog exposes **
|
|
5765
|
+
EyeProlog exposes **99 library predicate indicators** in addition to the 129
|
|
5766
5766
|
indicators in its isolated ISO profile. **60 are implemented entirely as
|
|
5767
|
-
ordinary Prolog clauses** in focused modules under `src/lib/`. The remaining
|
|
5767
|
+
ordinary Prolog clauses** in focused modules under `src/lib/`. The remaining 39
|
|
5768
5768
|
are public wrappers around backtrackable host support: Prologue `call_nth/2` and
|
|
5769
|
-
`freeze/2`, plus the
|
|
5770
|
-
resulting normal EyeProlog language surface is therefore **
|
|
5769
|
+
`freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
|
|
5770
|
+
resulting normal EyeProlog language surface is therefore **228 public predicate
|
|
5771
5771
|
indicators**. Internally, the runtime registry contains the 129 ISO definitions
|
|
5772
|
-
plus
|
|
5772
|
+
plus 23 private library adapters; public relations remain module source clauses.
|
|
5773
5773
|
|
|
5774
5774
|
The sources are `src/lib/aggregate.pl`, `src/lib/clpz.pl`, `src/lib/comparison.pl`,
|
|
5775
5775
|
`src/lib/dates.pl`, `src/lib/iso_ext.pl`, `src/lib/lists.pl`,
|
|
@@ -5799,7 +5799,7 @@ between solution branches.
|
|
|
5799
5799
|
| Module | Exported predicate indicators |
|
|
5800
5800
|
| --- | --- |
|
|
5801
5801
|
| `library(aggregate)` | `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
|
|
5802
|
-
| `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `sum/3`, `scalar_product/4`, `labeling/2`, `label/1`, `indomain/1`, `chain/2`, `element/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
|
|
5802
|
+
| `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `chain/2`, `element/3`, `zcompare/3`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2` |
|
|
5803
5803
|
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
|
|
5804
5804
|
| `library(dates)` | `difference/3` |
|
|
5805
5805
|
| `library(iso_ext)` | `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
|
|
@@ -5816,10 +5816,13 @@ between solution branches.
|
|
|
5816
5816
|
programming over integers. Its first implementation step provides finite
|
|
5817
5817
|
interval and union domains, arithmetic and reified constraints, backtrackable
|
|
5818
5818
|
labeling with `ff`, `up`, and `down`, global distinctness, linear sums and
|
|
5819
|
-
scalar products, chains, elements,
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5819
|
+
scalar products, chains, elements, value counting, extensional tuple tables,
|
|
5820
|
+
lexicographic chains, serialized schedules, global cardinality with costs,
|
|
5821
|
+
Hamiltonian circuits, three-way comparison, and domain reflection. Constraints
|
|
5822
|
+
are kept in the logical environment, so failed alternatives cannot leak domains
|
|
5823
|
+
into later branches. The full Trealla library also contains automata,
|
|
5824
|
+
cumulative and two-dimensional scheduling constraints, and unbounded-domain
|
|
5825
|
+
propagation that are not yet exported here.
|
|
5823
5826
|
|
|
5824
5827
|
`library(iso_ext)` collects extensions commonly found in mature Prolog
|
|
5825
5828
|
systems. `forall/2` checks an action for every solution of a condition;
|
|
@@ -6362,7 +6365,7 @@ Review questions:
|
|
|
6362
6365
|
</figure>
|
|
6363
6366
|
|
|
6364
6367
|
The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
|
|
6365
|
-
top-level directory contains **
|
|
6368
|
+
top-level directory contains **211 self-contained runnable programs**. Every
|
|
6366
6369
|
source program has an exact answer file under
|
|
6367
6370
|
[examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
|
|
6368
6371
|
explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic tables below link every top-level program and open the program
|
|
@@ -6411,8 +6414,10 @@ mode at a time.
|
|
|
6411
6414
|
| Program | Standard facility | Checked answer |
|
|
6412
6415
|
| --- | --- | --- |
|
|
6413
6416
|
| [CLP(Z) factorial](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-factorial.pl) | Declarative predecessor and product constraints propagate a factorial without mode-sensitive `is/2`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-factorial.pl) |
|
|
6417
|
+
| [CLP(Z) global constraints](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-global-constraints.pl) | Compatibility tables, lexicographic and serialized schedules, global cardinality with costs, circuits, value counting, and integer comparison. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-global-constraints.pl) |
|
|
6414
6418
|
| [CLP(Z) N-queens](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-n-queens.pl) | Six-queens using finite domains, delayed diagonal constraints, `all_distinct/1`, and first-fail labeling. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-n-queens.pl) |
|
|
6415
6419
|
| [CLP(Z) resource allocation](https://github.com/eyereasoner/eyeprolog/blob/main/examples/clpz-resource-allocation.pl) | Resource assignment using `element/3`, `sum/3`, `scalar_product/4`, reification, labeling options, and domain reflection. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/clpz-resource-allocation.pl) |
|
|
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) |
|
|
6416
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) |
|
|
6417
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) |
|
|
6418
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) |
|