eyeprolog 1.1.20 → 1.1.22

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.
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
11
11
  | builtins | 11 | 0 | 0 | 0 | 11 |
12
12
  | context | 11 | 0 | 0 | 0 | 11 |
13
13
  | control | 15 | 0 | 0 | 0 | 15 |
14
- | iso | 165 | 209 | 0 | 0 | 374 |
14
+ | iso | 166 | 209 | 0 | 0 | 375 |
15
15
  | lists | 52 | 3 | 0 | 0 | 55 |
16
16
  | modules | 2 | 0 | 0 | 0 | 2 |
17
17
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
23
23
  | terms | 26 | 3 | 0 | 0 | 29 |
24
24
  | unification | 18 | 0 | 0 | 0 | 18 |
25
25
  | variables | 16 | 9 | 0 | 0 | 25 |
26
- | **Total** | **480** | **260** | **19** | **21** | **780** |
26
+ | **Total** | **481** | **260** | **19** | **21** | **781** |
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.22",
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/parser.js CHANGED
@@ -225,6 +225,11 @@ class Parser {
225
225
  }
226
226
  operatorTokenName(token = this.token) {
227
227
  if (token.type === TOK.ATOM) return token.text;
228
+ // `:-` has its own token because it also introduces clauses/directives,
229
+ // but ISO 6.3.3.1 still permits an operator atom as an argument. Treat the
230
+ // token as the ordinary operator name while parsing terms; the surrounding
231
+ // grammar decides whether it is operator notation or atom data.
232
+ if (token.type === TOK.IF) return ':-';
228
233
  if (token.type === TOK.STRING && this.parserFlagState.doubleQuotes === 'atom') return token.text;
229
234
  return null;
230
235
  }
@@ -533,6 +538,14 @@ class Parser {
533
538
  return left;
534
539
  }
535
540
  parsePrefixTerm(minPrecedence = 0, allowBar = true) {
541
+ // `:-` is tokenized specially so the program grammar can recognize clause
542
+ // and directive markers. In term argument position, however, ISO 6.3.3.1
543
+ // permits an operator atom directly as an `arg`; a leading `:-` cannot be
544
+ // prefix operator notation at argument priority, so it denotes the atom.
545
+ if (this.token.type === TOK.IF) {
546
+ this.advance();
547
+ return atom(':-');
548
+ }
536
549
  const operatorName = this.operatorTokenName();
537
550
  if (operatorName != null && this.prefixOperators.get(operatorName)?.precedence >= minPrecedence) {
538
551
  const op = operatorName;
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
  }
@@ -94,7 +94,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
94
94
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
95
95
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
96
96
 
97
- The corpus has 374 cases in `iso/` and 780 file-based conformance cases in
97
+ The corpus has 375 cases in `iso/` and 781 file-based conformance cases in
98
98
  total. The generated `conformance-report.md` is the authoritative source for
99
99
  current category totals. Together with regression, documentation-sync, API,
100
100
  example, and book-example checks, `npm test` is the release gate.
@@ -0,0 +1,7 @@
1
+ % ISO 6.3.3.1: an arg may be an atom which is an operator.
2
+
3
+ %% goal: operator_atoms(Priority, Specifier, List)
4
+
5
+ operator_atoms(Priority, Specifier, List) :-
6
+ current_op(Priority, Specifier, :-),
7
+ List = [:-,-].
@@ -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)]).
@@ -0,0 +1,2 @@
1
+ operator_atoms(1200, xfx, [':-', '-']).
2
+ operator_atoms(1200, fx, [':-', '-']).
@@ -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: () => {
@@ -906,6 +940,16 @@ c4 ?- call((!;1)).
906
940
  assertEqual(program.findGroup('b', 0).clauses.length, 1, 'b/0 count');
907
941
  },
908
942
  },
943
+ {
944
+ name: 'ISO operator atoms are valid functional and list arguments',
945
+ run: () => {
946
+ const source = [
947
+ 'operator_argument(ok) :- current_op(1200, xfx, :-), [:-,-] = [:-,-].',
948
+ '',
949
+ ].join('\n');
950
+ assertEqual(run(source, { goal: 'operator_argument(ok)' }).stdout, 'operator_argument(ok).\n', 'operator argument syntax');
951
+ },
952
+ },
909
953
  {
910
954
  name: 'term input keeps dotted operators intact and uses program operators',
911
955
  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,
@@ -5099,7 +5120,11 @@ The fact is exactly `reports(sensor_7, temperature)`. Priority determines
5099
5120
  binding strength, and `fx`, `fy`, `xf`, `yf`, `xfx`, `xfy`, and `yfx`
5100
5121
  determine position and associativity. `current_op/3` inspects the table;
5101
5122
  `op(0, Specifier, Name)` removes a definition. Because declarations affect
5102
- parsing of subsequent text, place them before their first use.
5123
+ parsing of subsequent text, place them before their first use. ISO argument
5124
+ syntax also permits an atom that is currently an operator to appear directly
5125
+ as a functional argument or list element, so forms such as
5126
+ `current_op(Priority, Specifier, :-)` and `[:-,-]` are valid without quoting
5127
+ or parenthesizing those operator atoms.
5103
5128
 
5104
5129
  Run [`iso-dynamic-database.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl)
5105
5130
  for an explicitly stateful queue and
@@ -5610,11 +5635,14 @@ silently changing a static program.
5610
5635
  | `max_arity` | `unbounded` | `unbounded` | no |
5611
5636
  | `unknown` | `fail` | `error`, `fail`, `warning` | yes |
5612
5637
  | `double_quotes` | `chars` | `chars`, `codes`, `atom` | yes |
5638
+ | `occurs_check` | `true` | `true`, `error` | yes |
5613
5639
 
5614
5640
  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
5641
+ EyeProlog environment defaults it to `fail`. The `occurs_check` flag is an
5642
+ EyeProlog diagnostic extension rather than an ISO-defined core flag; its
5643
+ `true` default preserves the engine's finite-tree unification, while `error`
5644
+ reports STO attempts. Operator and flag directives are processed per program
5645
+ rather than changing global JavaScript state. The `double_quotes` setting affects subsequent source text, included
5618
5646
  files, command-line and API goal text, and terms read by `read_term/*`:
5619
5647
 
5620
5648
  ```text
@@ -6821,7 +6849,7 @@ node test/run-conformance-report.mjs
6821
6849
  ```
6822
6850
 
6823
6851
  The complete suite must pass before release. The file-based conformance corpus
6824
- contains 780 cases, including 374 focused ISO
6852
+ contains 781 cases, including 375 focused ISO
6825
6853
  cases derived from the success, failure, mode, and error behavior in
6826
6854
  ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
6827
6855
  Separate exact-output suites check 189 normal