eyeprolog 1.3.3 → 1.3.7

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.
Files changed (38) hide show
  1. package/README.md +78 -4
  2. package/examples/book/README.md +2 -1
  3. package/examples/book/chapter-07/02-move.pl +4 -0
  4. package/examples/book/chapter-08/01-findall.pl +1 -0
  5. package/examples/book/chapter-39/04-cost.pl +1 -0
  6. package/examples/chart-parser.pl +1 -0
  7. package/examples/dog.pl +1 -0
  8. package/examples/donald-gerald-robert.pl +1 -0
  9. package/examples/equality-saturation.pl +1 -0
  10. package/examples/job-shop-scheduling.pl +1 -0
  11. package/examples/knapsack-optimization.pl +1 -0
  12. package/examples/knuth-bendix-completion.pl +1 -0
  13. package/examples/matrix-chain-order.pl +1 -0
  14. package/examples/missionaries-cannibals.pl +1 -0
  15. package/examples/prime-range.pl +1 -0
  16. package/examples/register-allocation.pl +1 -0
  17. package/examples/send-more-money.pl +1 -0
  18. package/examples/stable-marriage.pl +1 -0
  19. package/examples/totient-summatory.pl +1 -0
  20. package/examples/weighted-interval-scheduling.pl +1 -0
  21. package/index.d.ts +7 -0
  22. package/package.json +1 -1
  23. package/src/datalog.js +354 -0
  24. package/src/iso.js +148 -14
  25. package/src/lib/iso_ext.pl +4 -0
  26. package/src/lib/lists.pl +0 -3
  27. package/src/program.js +258 -20
  28. package/src/solver.js +890 -46
  29. package/src/standard-library.js +12 -7
  30. package/src/term.js +1 -1
  31. package/src/wfs.js +338 -0
  32. package/test/conformance/ISO-COMPLIANCE.md +2 -1
  33. package/test/conformance/README.md +4 -1
  34. package/test/run-conformance.mjs +1 -0
  35. package/test/run-regression.mjs +230 -3
  36. package/the-art-of-eyeprolog.md +95 -21
  37. package/why-eyeprolog.md +17 -0
  38. /package/examples/book/chapter-07/{02-all_tests_pass.pl → 03-all_tests_pass.pl} +0 -0
@@ -109,10 +109,10 @@ export const eyePrologInteropAutoload = Object.freeze({
109
109
  'foldl/6': 'lists',
110
110
  'sum_list/2': 'lists',
111
111
  'list_to_set/2': 'lists',
112
- // call_nth/2 is available in both Trealla and Scryer (the latter via
113
- // library(iso_ext)). EyeProlog keeps its adapter in library(prologue), so
114
- // autoloading hides that implementation-specific location from source.
115
- 'call_nth/2': 'prologue',
112
+ // call_nth/2 is available in both Trealla and Scryer. EyeProlog exposes
113
+ // it from library(iso_ext), matching the explicit Scryer import while still
114
+ // allowing Trealla-style unqualified source to use the same autoload entry.
115
+ 'call_nth/2': 'iso_ext',
116
116
  // Trealla and Scryer expose between/3 without an EyeProlog-style
117
117
  // library(prologue) dependency. EyeProlog keeps its implementation in the
118
118
  // Prologue module but autoloads it so portable source need not name that
@@ -125,9 +125,13 @@ export const eyePrologInteropLibraryIndicators = Object.freeze(
125
125
  );
126
126
 
127
127
  // Libraries whose *name* is part of the current interop profile. A program
128
- // may freely use_module(library(lists)); predicates in that module which are
129
- // outside eyePrologInteropLibraryIndicators are still diagnosed when used.
130
- export const eyePrologInteropLibraryModules = Object.freeze(['lists']);
128
+ // may freely use_module/1 with these common module names; predicates in those
129
+ // modules outside eyePrologInteropLibraryIndicators are still diagnosed when used.
130
+ export const eyePrologInteropLibraryModules = Object.freeze(['lists', 'iso_ext']);
131
+
132
+ function* tabledNegationBuiltin({ solver, goal, env }) {
133
+ yield* solver.solveTabledNegation(goal.args[0], env);
134
+ }
131
135
 
132
136
  function runtimeStatistics(solver) {
133
137
  return { ...solver.stats, ...memoryStatistics() };
@@ -167,6 +171,7 @@ export function createEyePrologRegistry() {
167
171
  const registry = createDefaultRegistry();
168
172
  registry.add('statistics', 0, statisticsBuiltin, { deterministic: true });
169
173
  registry.add('statistics', 2, statisticsValueBuiltin);
174
+ registry.add('tnot', 1, tabledNegationBuiltin, { deterministic: true });
170
175
  eyePrologLibraryBuiltins.register(registry);
171
176
  clpzBuiltins.register(registry);
172
177
  registry.eyePrologLibrary = true;
package/src/term.js CHANGED
@@ -82,7 +82,7 @@ export const compactVariableList = (length, variablePrefix) => {
82
82
  return size === 0n ? emptyList() : new CompactListTerm(size, variablePrefix);
83
83
  };
84
84
  export const isCompactList = (term) => term instanceof CompactListTerm;
85
- export const compactListLength = (term) => isCompactList(term) ? term._compactLength : null;
85
+ export const compactListLength = (term) => typeof term?._compactLength === 'bigint' ? term._compactLength : null;
86
86
 
87
87
  export class Env {
88
88
  constructor(bindings) {
package/src/wfs.js ADDED
@@ -0,0 +1,338 @@
1
+ // Finite Datalog well-founded semantics (WFS) evaluator.
2
+ //
3
+ // This module deliberately targets the function-free, range-restricted subset
4
+ // marked by Program#markRecursivePredicates as group.wfsDatalog. It uses the
5
+ // alternating-fixpoint characterization of WFS: starting from an upper bound on
6
+ // possible atoms, repeatedly compute the least model of the Gelfond-Lifschitz
7
+ // reduct. The final lower relation contains unconditional truths; the final
8
+ // upper relation contains truths plus undefined (conditional) answers.
9
+ //
10
+ // Keeping this separate from ordinary \+/1 is intentional. Only explicit
11
+ // tnot/1 participates in WFS, matching the common tabled-negation convention.
12
+
13
+ import { ATOM, COMPOUND, VAR } from './term.js';
14
+ import { numberValueKey } from './number-value.js';
15
+
16
+ function predicateKey(module, name, arity) {
17
+ return `${module ?? 'user'}:${name}/${arity}`;
18
+ }
19
+
20
+ function scalarKey(term) {
21
+ if (term.type === 'number') return `number\u0000${numberValueKey(term.name)}`;
22
+ return `${term.type}\u0000${term.name}`;
23
+ }
24
+
25
+ function sameScalar(left, right) {
26
+ return scalarKey(left) === scalarKey(right);
27
+ }
28
+
29
+ function tupleKey(tuple) {
30
+ return tuple.map(scalarKey).join('\u0001');
31
+ }
32
+
33
+ class Relation {
34
+ constructor(arity) {
35
+ this.arity = arity;
36
+ this.rows = [];
37
+ this.keys = new Set();
38
+ this.indexes = Array.from({ length: arity }, () => new Map());
39
+ }
40
+
41
+ add(tuple) {
42
+ const key = tupleKey(tuple);
43
+ if (this.keys.has(key)) return false;
44
+ const rowIndex = this.rows.length;
45
+ this.keys.add(key);
46
+ this.rows.push(tuple);
47
+ for (let i = 0; i < tuple.length; i++) {
48
+ const keyPart = scalarKey(tuple[i]);
49
+ let bucket = this.indexes[i].get(keyPart);
50
+ if (!bucket) {
51
+ bucket = [];
52
+ this.indexes[i].set(keyPart, bucket);
53
+ }
54
+ bucket.push(rowIndex);
55
+ }
56
+ return true;
57
+ }
58
+
59
+ has(tuple) {
60
+ return this.keys.has(tupleKey(tuple));
61
+ }
62
+
63
+ candidateIndexes(args, bindings) {
64
+ let selected = null;
65
+ for (let i = 0; i < args.length; i++) {
66
+ const value = resolvePatternTerm(args[i], bindings);
67
+ if (value == null) continue;
68
+ const bucket = this.indexes[i].get(scalarKey(value)) ?? EMPTY_ARRAY;
69
+ if (selected == null || bucket.length < selected.length) selected = bucket;
70
+ if (selected.length === 0) break;
71
+ }
72
+ return selected ?? null;
73
+ }
74
+ }
75
+
76
+ const EMPTY_ARRAY = Object.freeze([]);
77
+
78
+ function emptyRelations(groups) {
79
+ const relations = new Map();
80
+ for (const group of groups) {
81
+ relations.set(predicateKey(group.module, group.name, group.arity), new Relation(group.arity));
82
+ }
83
+ return relations;
84
+ }
85
+
86
+ function copyBaseRelations(base, groups) {
87
+ const relations = emptyRelations(groups);
88
+ for (const [key, relation] of base) {
89
+ const target = relations.get(key);
90
+ if (!target) continue;
91
+ for (const row of relation.rows) target.add(row);
92
+ }
93
+ return relations;
94
+ }
95
+
96
+ function directLiteral(goal, module) {
97
+ if (goal?.type !== COMPOUND && goal?.type !== ATOM) return null;
98
+ return {
99
+ key: predicateKey(goal.module ?? module, goal.name, goal.arity),
100
+ name: goal.name,
101
+ arity: goal.arity,
102
+ module: goal.module ?? module,
103
+ args: goal.args ?? EMPTY_ARRAY,
104
+ };
105
+ }
106
+
107
+ function negativeLiteral(goal, module) {
108
+ if (goal?.type !== COMPOUND || goal.name !== 'tnot' || goal.arity !== 1) return null;
109
+ return directLiteral(goal.args[0], module);
110
+ }
111
+
112
+ function dependencyCone(program, rootGroup) {
113
+ const groups = [];
114
+ const seen = new Set();
115
+ const stack = [rootGroup];
116
+ while (stack.length > 0) {
117
+ const group = stack.pop();
118
+ const key = predicateKey(group.module, group.name, group.arity);
119
+ if (seen.has(key)) continue;
120
+ seen.add(key);
121
+ groups.push(group);
122
+
123
+ for (const clause of group.clauses) {
124
+ for (const goal of clause.body) {
125
+ const neg = negativeLiteral(goal, group.module);
126
+ const literal = neg ?? directLiteral(goal, group.module);
127
+ if (!literal) continue;
128
+ const target = program.findGroup(literal.name, literal.arity, literal.module);
129
+ if (target) stack.push(target);
130
+ }
131
+ }
132
+ }
133
+ return groups;
134
+ }
135
+
136
+ function compileProgram(program, rootGroup) {
137
+ const groups = dependencyCone(program, rootGroup);
138
+ const base = emptyRelations(groups);
139
+ const rules = [];
140
+
141
+ for (const group of groups) {
142
+ const headKey = predicateKey(group.module, group.name, group.arity);
143
+ for (const clause of group.clauses) {
144
+ if (clause.body.length === 0) {
145
+ const tuple = clause.head.args ?? EMPTY_ARRAY;
146
+ // Program analysis already guarantees ground scalar facts in the WFS
147
+ // cone. Keep the guard so malformed dynamic input cannot corrupt a model.
148
+ if (tuple.every((term) => term.type !== VAR && (term.type === ATOM || term.type === 'number' || term.type === 'string'))) {
149
+ base.get(headKey)?.add(tuple);
150
+ }
151
+ continue;
152
+ }
153
+
154
+ const positives = [];
155
+ const negatives = [];
156
+ for (const goal of clause.body) {
157
+ const neg = negativeLiteral(goal, group.module);
158
+ if (neg) negatives.push(neg);
159
+ else {
160
+ const pos = directLiteral(goal, group.module);
161
+ if (pos) positives.push(pos);
162
+ }
163
+ }
164
+ rules.push({
165
+ head: {
166
+ key: headKey,
167
+ args: clause.head.args ?? EMPTY_ARRAY,
168
+ },
169
+ positives,
170
+ negatives,
171
+ });
172
+ }
173
+ }
174
+
175
+ return { groups, base, rules };
176
+ }
177
+
178
+ function resolvePatternTerm(term, bindings) {
179
+ if (term.type === VAR) return bindings.get(term.name) ?? null;
180
+ return term;
181
+ }
182
+
183
+ function matchTuple(args, tuple, bindings) {
184
+ let next = null;
185
+ for (let i = 0; i < args.length; i++) {
186
+ const pattern = args[i];
187
+ if (pattern.type === VAR) {
188
+ const current = (next ?? bindings).get(pattern.name);
189
+ if (current != null) {
190
+ if (!sameScalar(current, tuple[i])) return null;
191
+ } else {
192
+ if (next == null) next = new Map(bindings);
193
+ next.set(pattern.name, tuple[i]);
194
+ }
195
+ continue;
196
+ }
197
+ if (!sameScalar(pattern, tuple[i])) return null;
198
+ }
199
+ return next ?? bindings;
200
+ }
201
+
202
+ function estimateLiteral(literal, relation, bindings) {
203
+ const candidates = relation.candidateIndexes(literal.args, bindings);
204
+ return candidates == null ? relation.rows.length : candidates.length;
205
+ }
206
+
207
+ function forEachPositiveBinding(positives, relations, callback, bindings = new Map(), remaining = null) {
208
+ if (remaining == null) remaining = positives.map((_, index) => index);
209
+ if (remaining.length === 0) {
210
+ callback(bindings);
211
+ return;
212
+ }
213
+
214
+ let bestPos = 0;
215
+ let bestEstimate = Infinity;
216
+ for (let position = 0; position < remaining.length; position++) {
217
+ const literal = positives[remaining[position]];
218
+ const relation = relations.get(literal.key);
219
+ const estimate = relation ? estimateLiteral(literal, relation, bindings) : 0;
220
+ if (estimate < bestEstimate) {
221
+ bestEstimate = estimate;
222
+ bestPos = position;
223
+ if (estimate === 0) return;
224
+ }
225
+ }
226
+
227
+ const literalIndex = remaining[bestPos];
228
+ const literal = positives[literalIndex];
229
+ const relation = relations.get(literal.key);
230
+ if (!relation) return;
231
+ const candidates = relation.candidateIndexes(literal.args, bindings);
232
+ const nextRemaining = remaining.length === 1
233
+ ? EMPTY_ARRAY
234
+ : [...remaining.slice(0, bestPos), ...remaining.slice(bestPos + 1)];
235
+
236
+ if (candidates == null) {
237
+ for (let rowIndex = 0; rowIndex < relation.rows.length; rowIndex++) {
238
+ const next = matchTuple(literal.args, relation.rows[rowIndex], bindings);
239
+ if (next) forEachPositiveBinding(positives, relations, callback, next, nextRemaining);
240
+ }
241
+ return;
242
+ }
243
+ for (const rowIndex of candidates) {
244
+ const next = matchTuple(literal.args, relation.rows[rowIndex], bindings);
245
+ if (next) forEachPositiveBinding(positives, relations, callback, next, nextRemaining);
246
+ }
247
+ }
248
+
249
+ function instantiateTuple(args, bindings) {
250
+ const tuple = [];
251
+ for (const arg of args) {
252
+ const value = arg.type === VAR ? bindings.get(arg.name) : arg;
253
+ if (value == null || value.type === VAR) return null;
254
+ tuple.push(value);
255
+ }
256
+ return tuple;
257
+ }
258
+
259
+ function blockedByNegative(negatives, blockerRelations, bindings) {
260
+ if (!blockerRelations) return false;
261
+ for (const literal of negatives) {
262
+ const relation = blockerRelations.get(literal.key);
263
+ if (!relation) continue;
264
+ const tuple = instantiateTuple(literal.args, bindings);
265
+ if (tuple == null) return true; // range restriction should make this unreachable
266
+ if (relation.has(tuple)) return true;
267
+ }
268
+ return false;
269
+ }
270
+
271
+ function gamma(compiled, blockerRelations = null) {
272
+ const relations = copyBaseRelations(compiled.base, compiled.groups);
273
+ let changed = true;
274
+ while (changed) {
275
+ changed = false;
276
+ for (const rule of compiled.rules) {
277
+ const headRelation = relations.get(rule.head.key);
278
+ if (!headRelation) continue;
279
+ forEachPositiveBinding(rule.positives, relations, (bindings) => {
280
+ if (blockedByNegative(rule.negatives, blockerRelations, bindings)) return;
281
+ const tuple = instantiateTuple(rule.head.args, bindings);
282
+ if (tuple != null && headRelation.add(tuple)) changed = true;
283
+ });
284
+ }
285
+ }
286
+ return relations;
287
+ }
288
+
289
+ function relationSetsEqual(left, right) {
290
+ if (left.size !== right.size) return false;
291
+ for (const [key, leftRelation] of left) {
292
+ const rightRelation = right.get(key);
293
+ if (!rightRelation || leftRelation.keys.size !== rightRelation.keys.size) return false;
294
+ for (const tuple of leftRelation.keys) if (!rightRelation.keys.has(tuple)) return false;
295
+ }
296
+ return true;
297
+ }
298
+
299
+ export function evaluateWfs(program, rootGroup) {
300
+ const compiled = compileProgram(program, rootGroup);
301
+
302
+ // Positive closure with tnot erased is an upper bound on every atom that can
303
+ // participate in the WFS model, avoiding construction of the full Herbrand
304
+ // cross product for predicates of arity > 1.
305
+ let upper = gamma(compiled, null);
306
+ let lower = emptyRelations(compiled.groups);
307
+ let rounds = 0;
308
+
309
+ while (true) {
310
+ rounds++;
311
+ const nextLower = gamma(compiled, upper);
312
+ const nextUpper = gamma(compiled, nextLower);
313
+ if (relationSetsEqual(lower, nextLower) && relationSetsEqual(upper, nextUpper)) {
314
+ lower = nextLower;
315
+ upper = nextUpper;
316
+ break;
317
+ }
318
+ lower = nextLower;
319
+ upper = nextUpper;
320
+ }
321
+
322
+ return { lower, upper, rounds, groups: compiled.groups };
323
+ }
324
+
325
+ export function relationForGroup(model, group, kind = 'upper') {
326
+ const relations = kind === 'lower' ? model.lower : model.upper;
327
+ return relations.get(predicateKey(group.module, group.name, group.arity)) ?? null;
328
+ }
329
+
330
+ export function truthOfGroundGoal(model, goal) {
331
+ const key = predicateKey(goal.module ?? 'user', goal.name, goal.arity);
332
+ const tuple = goal.args ?? EMPTY_ARRAY;
333
+ const lower = model.lower.get(key);
334
+ if (lower?.has(tuple)) return 'true';
335
+ const upper = model.upper.get(key);
336
+ if (upper?.has(tuple)) return 'undefined';
337
+ return 'false';
338
+ }
@@ -56,7 +56,8 @@ not interpret the following as core-language features:
56
56
  - the implementation-specific `occurs_check` Prolog flag;
57
57
  - Part 2 module directives (`module/2`, `use_module/1-2`, `meta_predicate/1`);
58
58
  - Part 3 grammar-rule expansion and `phrase/2-3`;
59
- - automatic tabling, cycle guards, and recursive numeric execution shortcuts.
59
+ - automatic tabling, cycle guards, and recursive numeric execution shortcuts;
60
+ - EyeProlog well-founded negation via `tnot/1` and its WFS runtime statistics.
60
61
 
61
62
  The predefined Part 1 `1200 fx` `?-` operator and `1200 xfx` `-->` operator
62
63
  remain ordinary operator syntax in strict core mode. A conforming `op/3`
@@ -21,7 +21,10 @@ processor-requirement ledger for the Part 1 strict-core audit. This suite is
21
21
  still not an independent certification of every processor requirement, lexical
22
22
  edge, option combination, or prescribed error precedence. Cases under `iso/`
23
23
  identify standards-derived behavior; other directories cover EyeProlog host
24
- contracts and extensions.
24
+ contracts and extensions. EyeProlog-only execution features such as automatic
25
+ tabling and `tnot/1` well-founded negation are outside the Part 1 strict-core
26
+ claim. Their focused semantic coverage lives primarily in regression tests;
27
+ `tnot/1` is absent from the strict ISO registry.
25
28
 
26
29
  All conformance files live under topic directories such as `arithmetic/`, `lists/`, `syntax/`, or `variables/`; new top-level numbered files should not be added. The report uses those directories as coverage categories.
27
30
 
@@ -18,6 +18,7 @@ function withStandardModules(text) {
18
18
  return `:- use_module(library(aggregate)).
19
19
  :- use_module(library(comparison)).
20
20
  :- use_module(library(dates)).
21
+ :- use_module(library(iso_ext)).
21
22
  :- use_module(library(lists)).
22
23
  :- use_module(library(primes)).
23
24
  :- use_module(library(prologue), [between/3]).