eyeprolog 1.1.20 → 1.1.21
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/package.json +1 -1
- package/src/iso.js +10 -1
- package/src/repl.js +9 -0
- package/src/solver.js +14 -0
- package/src/term.js +16 -3
- package/test/conformance/expected/iso/exceptions_and_flags.pl +1 -1
- package/test/run-regression.mjs +34 -0
- package/the-art-of-eyeprolog.md +27 -3
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -48,7 +48,7 @@ export const isoBuiltins = {
|
|
|
48
48
|
registry.add('!', 0, succeed, { deterministic: true });
|
|
49
49
|
|
|
50
50
|
registry.add('=', 2, unification, { deterministic: true });
|
|
51
|
-
registry.add('unify_with_occurs_check', 2,
|
|
51
|
+
registry.add('unify_with_occurs_check', 2, unificationWithOccursCheck, { deterministic: true });
|
|
52
52
|
registry.add('\\=', 2, nonUnification, { deterministic: true });
|
|
53
53
|
registry.add('subsumes_term', 2, subsumesTermBuiltin, { deterministic: true });
|
|
54
54
|
registry.add('==', 2, identity, { deterministic: true });
|
|
@@ -178,6 +178,14 @@ function* unification({ goal, env }) {
|
|
|
178
178
|
const next = env.clone();
|
|
179
179
|
if (unify(goal.args[0], goal.args[1], next)) yield next;
|
|
180
180
|
}
|
|
181
|
+
function* unificationWithOccursCheck({ goal, env }) {
|
|
182
|
+
const next = env.clone();
|
|
183
|
+
// ISO unify_with_occurs_check/2 always performs finite-tree unification.
|
|
184
|
+
// The implementation-specific occurs_check=error mode applies to normal
|
|
185
|
+
// unification, but must not turn this ISO predicate's ordinary failure into
|
|
186
|
+
// an exception.
|
|
187
|
+
if (unify(goal.args[0], goal.args[1], next, { occursCheck: 'fail' })) yield next;
|
|
188
|
+
}
|
|
181
189
|
function* nonUnification({ goal, env }) {
|
|
182
190
|
if (!unify(goal.args[0], goal.args[1], env.clone())) yield env;
|
|
183
191
|
}
|
|
@@ -1776,6 +1784,7 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
1776
1784
|
}
|
|
1777
1785
|
}
|
|
1778
1786
|
function formalErrorTerm(error) {
|
|
1787
|
+
if (error.formalTerm != null) return compound('error', [error.formalTerm, atom('eyeprolog')]);
|
|
1779
1788
|
const parse = (text) => {
|
|
1780
1789
|
const open = text.indexOf('(');
|
|
1781
1790
|
if (open === -1) return atom(text);
|
package/src/repl.js
CHANGED
|
@@ -413,6 +413,15 @@ function formatError(engine, state, error) {
|
|
|
413
413
|
const env = new engine.Env();
|
|
414
414
|
const variableNames = new Map();
|
|
415
415
|
let generated = 0;
|
|
416
|
+
if (error.formalTerm != null) {
|
|
417
|
+
collectUnboundVariables(engine, error.formalTerm, env, variableNames, () => `_${letterName(generated++)}`);
|
|
418
|
+
const formal = engine.formatTermForWrite(error.formalTerm, env, {
|
|
419
|
+
quoted: true,
|
|
420
|
+
operators: [...state.program.operators.values()],
|
|
421
|
+
variableNames,
|
|
422
|
+
});
|
|
423
|
+
return `error(${formal}).`;
|
|
424
|
+
}
|
|
416
425
|
if (error.culprit != null) {
|
|
417
426
|
collectUnboundVariables(engine, error.culprit, env, variableNames, () => `_${letterName(generated++)}`);
|
|
418
427
|
}
|
package/src/solver.js
CHANGED
|
@@ -23,6 +23,12 @@ export function nextFreshId() {
|
|
|
23
23
|
return ++freshCounter;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function raiseOccursCheckError(left, right, env) {
|
|
27
|
+
const error = new PrologError('occurs_check');
|
|
28
|
+
error.formalTerm = copyResolved(compound('occurs_check', [left, right]), env);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
export class Solver {
|
|
27
33
|
constructor(program, options = {}) {
|
|
28
34
|
this.registry = options.registry ?? getEyePrologRegistry();
|
|
@@ -37,6 +43,11 @@ export class Solver {
|
|
|
37
43
|
this.solutionLimit = options.solutionLimit ?? 10000000;
|
|
38
44
|
this.solutionsSeen = 0;
|
|
39
45
|
this.prologFlags = options.prologFlags ?? defaultPrologFlags(this.registry?.eyePrologLibrary ? 'fail' : 'error');
|
|
46
|
+
this.occursCheckHandler = (left, right, env) => {
|
|
47
|
+
if (this.prologFlags.get('occurs_check')?.value?.name === 'error') {
|
|
48
|
+
raiseOccursCheckError(left, right, env);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
40
51
|
this.charConversions = options.charConversions ?? new Map();
|
|
41
52
|
if (!options.prologFlags) {
|
|
42
53
|
if (['chars', 'codes', 'atom'].includes(program.doubleQuotes)) {
|
|
@@ -140,6 +151,7 @@ export class Solver {
|
|
|
140
151
|
|
|
141
152
|
*solve(goals, env = new Env(), depth = 0) {
|
|
142
153
|
if (!Array.isArray(goals)) goals = [goals];
|
|
154
|
+
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
143
155
|
|
|
144
156
|
const savedActive = this.active;
|
|
145
157
|
let registeredStack = null;
|
|
@@ -194,6 +206,7 @@ export class Solver {
|
|
|
194
206
|
|
|
195
207
|
goals = frame.goals;
|
|
196
208
|
env = frame.env;
|
|
209
|
+
env.setOccursCheckHandler(this.occursCheckHandler);
|
|
197
210
|
depth = frame.depth;
|
|
198
211
|
let active = frame.active;
|
|
199
212
|
|
|
@@ -474,6 +487,7 @@ function defaultPrologFlags(unknown = 'error') {
|
|
|
474
487
|
['max_arity', { value: compound('unbounded', []), allowed: ['unbounded'], changeable: false }],
|
|
475
488
|
['unknown', { value: compound(unknown, []), allowed: ['error', 'fail', 'warning'], changeable: true }],
|
|
476
489
|
['double_quotes', { value: compound('chars', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
|
|
490
|
+
['occurs_check', { value: compound('true', []), allowed: ['true', 'error'], changeable: true }],
|
|
477
491
|
]);
|
|
478
492
|
}
|
|
479
493
|
|
package/src/term.js
CHANGED
|
@@ -47,6 +47,7 @@ export class Env {
|
|
|
47
47
|
};
|
|
48
48
|
this._delays = null;
|
|
49
49
|
this._clpz = null;
|
|
50
|
+
this._occursCheckHandler = null;
|
|
50
51
|
}
|
|
51
52
|
clone() {
|
|
52
53
|
// Most speculative environments are either rejected without a binding or
|
|
@@ -58,8 +59,13 @@ export class Env {
|
|
|
58
59
|
clone._state = this._state;
|
|
59
60
|
clone._delays = this._delays;
|
|
60
61
|
clone._clpz = this._clpz;
|
|
62
|
+
clone._occursCheckHandler = this._occursCheckHandler;
|
|
61
63
|
return clone;
|
|
62
64
|
}
|
|
65
|
+
setOccursCheckHandler(handler) {
|
|
66
|
+
this._occursCheckHandler = typeof handler === 'function' ? handler : null;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
63
69
|
has(name) {
|
|
64
70
|
return this.get(name) !== undefined;
|
|
65
71
|
}
|
|
@@ -207,11 +213,12 @@ function occurs(variableName, term, env) {
|
|
|
207
213
|
return false;
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
export function unify(left, right, env) {
|
|
216
|
+
export function unify(left, right, env, options = {}) {
|
|
211
217
|
// Iterative unification avoids deep JavaScript recursion on long lists or
|
|
212
218
|
// deeply nested compounds. The occurs check gives EyeProlog finite-tree
|
|
213
219
|
// unification: a variable cannot be bound to a term containing itself.
|
|
214
220
|
// Bindings are written into the supplied Env.
|
|
221
|
+
const occursCheckHandler = options.occursCheck === 'fail' ? null : env?._occursCheckHandler;
|
|
215
222
|
const stack = [[left, right]];
|
|
216
223
|
while (stack.length) {
|
|
217
224
|
let [a, b] = stack.pop();
|
|
@@ -226,12 +233,18 @@ export function unify(left, right, env) {
|
|
|
226
233
|
continue;
|
|
227
234
|
}
|
|
228
235
|
if (a.type === VAR) {
|
|
229
|
-
if (occurs(a.name, b, env))
|
|
236
|
+
if (occurs(a.name, b, env)) {
|
|
237
|
+
occursCheckHandler?.(a, b, env);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
230
240
|
env.bind(a.name, b);
|
|
231
241
|
continue;
|
|
232
242
|
}
|
|
233
243
|
if (b.type === VAR) {
|
|
234
|
-
if (occurs(b.name, a, env))
|
|
244
|
+
if (occurs(b.name, a, env)) {
|
|
245
|
+
occursCheckHandler?.(b, a, env);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
235
248
|
env.bind(b.name, a);
|
|
236
249
|
continue;
|
|
237
250
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
answer(7, red, caught(eyeprolog), first, caught(eyeprolog)).
|
|
2
2
|
answer(7, red, caught(eyeprolog), second, caught(eyeprolog)).
|
|
3
|
-
flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_integer, unbounded), pair(min_integer, unbounded), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars)]).
|
|
3
|
+
flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_integer, unbounded), pair(min_integer, unbounded), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)]).
|
package/test/run-regression.mjs
CHANGED
|
@@ -522,6 +522,40 @@ c4 ?- call((!;1)).
|
|
|
522
522
|
assertEqual(result.stderr, '', 'stderr');
|
|
523
523
|
},
|
|
524
524
|
},
|
|
525
|
+
{
|
|
526
|
+
name: 'occurs_check error mode detects STO while ISO occurs-check unification still fails',
|
|
527
|
+
run: () => {
|
|
528
|
+
const filename = path.join(tmp, `occurs-check-${++tmpCounter}.pl`);
|
|
529
|
+
fs.writeFileSync(filename, ':- set_prolog_flag(occurs_check, error).\nsame(X, X).\n');
|
|
530
|
+
const result = runCli([], {
|
|
531
|
+
input:
|
|
532
|
+
'current_prolog_flag(occurs_check, Mode).\n' +
|
|
533
|
+
'set_prolog_flag(occurs_check, error).\n' +
|
|
534
|
+
'X = f(X).\n' +
|
|
535
|
+
'catch((Y = g(Y)), E, true).\n' +
|
|
536
|
+
'unify_with_occurs_check(Z, h(Z)).\n' +
|
|
537
|
+
`[${sourceAtom(filename)}].\n` +
|
|
538
|
+
'same(W, k(W)).\n' +
|
|
539
|
+
'set_prolog_flag(occurs_check, true).\n' +
|
|
540
|
+
'Q = q(Q).\n' +
|
|
541
|
+
'halt.\n',
|
|
542
|
+
});
|
|
543
|
+
assertEqual(result.status, 0, 'exit status');
|
|
544
|
+
assertEqual(result.stdout,
|
|
545
|
+
'?- Mode = true.\n' +
|
|
546
|
+
'?- true.\n' +
|
|
547
|
+
'?- error(occurs_check(_A, f(_A))).\n' +
|
|
548
|
+
'?- E = error(occurs_check(_A, g(_A)), eyeprolog).\n' +
|
|
549
|
+
'?- false.\n' +
|
|
550
|
+
'?- true.\n' +
|
|
551
|
+
'?- error(occurs_check(_A, k(_A))).\n' +
|
|
552
|
+
'?- true.\n' +
|
|
553
|
+
'?- false.\n' +
|
|
554
|
+
'?- ',
|
|
555
|
+
'stdout');
|
|
556
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
557
|
+
},
|
|
558
|
+
},
|
|
525
559
|
{
|
|
526
560
|
name: 'REPL enumerates and stops answers like the Scryer top level',
|
|
527
561
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -780,6 +780,27 @@ fails rather than constructing a cyclic term:
|
|
|
780
780
|
(X = wrapper(X)).
|
|
781
781
|
```
|
|
782
782
|
|
|
783
|
+
ISO classifies unifications whose outcome depends on an occurs check as
|
|
784
|
+
subject-to-occurs-check (STO). EyeProlog's default remains the sound finite-tree
|
|
785
|
+
behavior above. For diagnosis, EyeProlog additionally provides the
|
|
786
|
+
implementation-specific flag `occurs_check`; setting it to `error` turns a
|
|
787
|
+
normal unification that would otherwise fail because of the occurs check into
|
|
788
|
+
an exception:
|
|
789
|
+
|
|
790
|
+
```eyeprolog
|
|
791
|
+
:- set_prolog_flag(occurs_check, error).
|
|
792
|
+
|
|
793
|
+
sto_example :- X = wrapper(X).
|
|
794
|
+
% error(occurs_check(_A, wrapper(_A)))
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
The supported values are `true` (the default finite-tree behavior) and `error`
|
|
798
|
+
(STO detection). EyeProlog deliberately does not provide `occurs_check=false`,
|
|
799
|
+
because its term model does not construct cyclic terms. The ISO predicate
|
|
800
|
+
`unify_with_occurs_check/2` is independent of the diagnostic flag: it continues
|
|
801
|
+
to perform finite-tree unification and fails on `unify_with_occurs_check(X,
|
|
802
|
+
wrapper(X))` even when `occurs_check` is `error`.
|
|
803
|
+
|
|
783
804
|
### Meaning is not the search strategy
|
|
784
805
|
|
|
785
806
|
EyeProlog's evaluator is goal-directed. It resolves selected goals against facts,
|
|
@@ -5610,11 +5631,14 @@ silently changing a static program.
|
|
|
5610
5631
|
| `max_arity` | `unbounded` | `unbounded` | no |
|
|
5611
5632
|
| `unknown` | `fail` | `error`, `fail`, `warning` | yes |
|
|
5612
5633
|
| `double_quotes` | `chars` | `chars`, `codes`, `atom` | yes |
|
|
5634
|
+
| `occurs_check` | `true` | `true`, `error` | yes |
|
|
5613
5635
|
|
|
5614
5636
|
The isolated ISO-only registry defaults `unknown` to `error`; the normal
|
|
5615
|
-
EyeProlog environment defaults it to `fail`.
|
|
5616
|
-
|
|
5617
|
-
|
|
5637
|
+
EyeProlog environment defaults it to `fail`. The `occurs_check` flag is an
|
|
5638
|
+
EyeProlog diagnostic extension rather than an ISO-defined core flag; its
|
|
5639
|
+
`true` default preserves the engine's finite-tree unification, while `error`
|
|
5640
|
+
reports STO attempts. Operator and flag directives are processed per program
|
|
5641
|
+
rather than changing global JavaScript state. The `double_quotes` setting affects subsequent source text, included
|
|
5618
5642
|
files, command-line and API goal text, and terms read by `read_term/*`:
|
|
5619
5643
|
|
|
5620
5644
|
```text
|