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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.20",
6
+ "version": "1.1.21",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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, unification, { deterministic: true });
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)) return false;
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)) return false;
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)]).
@@ -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: () => {
@@ -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`. Operator and flag
5616
- directives are processed per program rather than changing global JavaScript
5617
- state. The `double_quotes` setting affects subsequent source text, included
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