eyeprolog 1.1.23 → 1.1.25

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.23",
6
+ "version": "1.1.25",
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
@@ -1164,9 +1164,9 @@ function* readTermBuiltin({ solver, goal, env }) {
1164
1164
  yield next;
1165
1165
  }
1166
1166
  function defaultTermWriteOptions(mode) {
1167
- if (mode === 'writeq') return { quoted: true, ignoreOps: false, numbervars: true, variableNames: new Map() };
1168
- if (mode === 'canonical') return { quoted: true, ignoreOps: true, numbervars: false, variableNames: new Map() };
1169
- return { quoted: false, ignoreOps: false, numbervars: true, variableNames: new Map() };
1167
+ if (mode === 'writeq') return { quoted: true, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
1168
+ if (mode === 'canonical') return { quoted: true, ignoreOps: true, numbervars: false, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
1169
+ return { quoted: false, ignoreOps: false, numbervars: true, variableNames: new Map(), compact: true, operatorAtomsAsArgs: true };
1170
1170
  }
1171
1171
 
1172
1172
  function writeOptionBoolean(value, env, option) {
@@ -1784,7 +1784,8 @@ function* phraseBuiltin({ solver, goal, env }) {
1784
1784
  }
1785
1785
  }
1786
1786
  function formalErrorTerm(error) {
1787
- if (error.formalTerm != null) return compound('error', [error.formalTerm, atom('eyeprolog')]);
1787
+ const context = error.contextTerm ?? atom('eyeprolog');
1788
+ if (error.formalTerm != null) return compound('error', [error.formalTerm, context]);
1788
1789
  const parse = (text) => {
1789
1790
  const open = text.indexOf('(');
1790
1791
  if (open === -1) return atom(text);
@@ -1811,7 +1812,7 @@ function formalErrorTerm(error) {
1811
1812
  formal = compound(formal.name, [error.culprit]);
1812
1813
  }
1813
1814
  }
1814
- return compound('error', [formal, atom('eyeprolog')]);
1815
+ return compound('error', [formal, context]);
1815
1816
  }
1816
1817
  function* catchBuiltin({ solver, goal, env }) {
1817
1818
  const child = solver.cloneForInnerGoal();
package/src/repl.js CHANGED
@@ -415,21 +415,43 @@ function formatError(engine, state, error) {
415
415
  let generated = 0;
416
416
  if (error.formalTerm != null) {
417
417
  collectUnboundVariables(engine, error.formalTerm, env, variableNames, () => `_${letterName(generated++)}`);
418
+ if (error.contextTerm != null) {
419
+ collectUnboundVariables(engine, error.contextTerm, env, variableNames, () => `_${letterName(generated++)}`);
420
+ }
418
421
  const formal = engine.formatTermForWrite(error.formalTerm, env, {
419
422
  quoted: true,
420
423
  operators: [...state.program.operators.values()],
421
424
  variableNames,
422
425
  });
426
+ if (error.contextTerm != null) {
427
+ const context = engine.formatTermForWrite(error.contextTerm, env, {
428
+ quoted: true,
429
+ operators: [...state.program.operators.values()],
430
+ variableNames,
431
+ });
432
+ return `error(${formal}, ${context}).`;
433
+ }
423
434
  return `error(${formal}).`;
424
435
  }
425
436
  if (error.culprit != null) {
426
437
  collectUnboundVariables(engine, error.culprit, env, variableNames, () => `_${letterName(generated++)}`);
427
438
  }
439
+ if (error.contextTerm != null) {
440
+ collectUnboundVariables(engine, error.contextTerm, env, variableNames, () => `_${letterName(generated++)}`);
441
+ }
428
442
  const culprit = error.culprit == null ? '' : `, ${engine.formatTermForWrite(error.culprit, env, {
429
443
  quoted: true,
430
444
  operators: [...state.program.operators.values()],
431
445
  variableNames,
432
446
  })}`;
447
+ if (error.contextTerm != null) {
448
+ const context = engine.formatTermForWrite(error.contextTerm, env, {
449
+ quoted: true,
450
+ operators: [...state.program.operators.values()],
451
+ variableNames,
452
+ });
453
+ return `error(${error.formal}${culprit}, ${context}).`;
454
+ }
433
455
  return `error(${error.formal}${culprit}).`;
434
456
  }
435
457
  const message = error?.message ?? String(error);
package/src/solver.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
2
2
  // Most semantic decisions still flow through unification; optimizations only select candidates earlier.
3
3
  import {
4
- COMPOUND, Env, compound, copyResolved, deref, flattenConjunction, freshTerm,
4
+ COMPOUND, Env, compound, copyResolved, deref, emptyList, flattenConjunction, freshTerm,
5
5
  numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variantTerms,
6
6
  } from './term.js';
7
7
  import { PrologError } from './iso.js';
@@ -24,8 +24,11 @@ export function nextFreshId() {
24
24
  }
25
25
 
26
26
  function raiseOccursCheckError(left, right, env) {
27
- const error = new PrologError('occurs_check');
28
- error.formalTerm = copyResolved(compound('occurs_check', [left, right]), env);
27
+ // occurs_check=error is an implementation-specific STO diagnostic. Report
28
+ // the unrepresentable cyclic result through the standard error envelope.
29
+ // Keep the implementation-defined context empty for stable, portable output.
30
+ const error = new PrologError('representation_error(term)');
31
+ error.contextTerm = emptyList();
29
32
  throw error;
30
33
  }
31
34
 
package/src/write.js CHANGED
@@ -124,11 +124,18 @@ function chooseOperator(term, table) {
124
124
  return null;
125
125
  }
126
126
 
127
- function format(term, env, options, table, maxPriority = 1200) {
127
+ function format(term, env, options, table, maxPriority = 1200, context = 'term') {
128
128
  const resolved = deref(term, env);
129
129
  if (resolved.type === VAR) return options.variableNames.get(resolved.name) ?? writeVariable(resolved.name);
130
130
  if (resolved.type === STRING) return writeString(resolved.name);
131
- if (resolved.type === ATOM) return options.quoted ? writeAtom(resolved.name) : resolved.name;
131
+ if (resolved.type === ATOM) {
132
+ if (!options.quoted) return resolved.name;
133
+ // ISO 6.3.3.1 gives functional arguments and list elements a special
134
+ // `arg` production: an atom that is a current operator is valid there
135
+ // without quoting. Keep lexical exceptions such as `|` quoted.
136
+ if (options.operatorAtomsAsArgs && context === 'argument' && table.has(resolved.name)) return operatorName(resolved.name);
137
+ return writeAtom(resolved.name);
138
+ }
132
139
  if (resolved.type === NUMBER) return resolved.name;
133
140
 
134
141
  if (options.numbervars && resolved.type === COMPOUND && resolved.name === '$VAR' && resolved.arity === 1) {
@@ -146,9 +153,13 @@ function format(term, env, options, table, maxPriority = 1200) {
146
153
  let cursor = resolved;
147
154
  while (true) {
148
155
  cursor = deref(cursor, env);
149
- if (isEmptyList(cursor)) return `[${parts.join(', ')}]`;
150
- if (!isCons(cursor)) return `[${parts.join(', ')} | ${format(cursor, env, options, table, 999)}]`;
151
- parts.push(format(cursor.args[0], env, options, table, 999));
156
+ const separator = options.compact ? ',' : ', ';
157
+ if (isEmptyList(cursor)) return `[${parts.join(separator)}]`;
158
+ if (!isCons(cursor)) {
159
+ const tailSeparator = options.compact ? '|' : ' | ';
160
+ return `[${parts.join(separator)}${tailSeparator}${format(cursor, env, options, table, 999, 'argument')}]`;
161
+ }
162
+ parts.push(format(cursor.args[0], env, options, table, 999, 'argument'));
152
163
  cursor = cursor.args[1];
153
164
  }
154
165
  }
@@ -183,8 +194,8 @@ function format(term, env, options, table, maxPriority = 1200) {
183
194
  }
184
195
 
185
196
  const name = options.quoted ? writeAtom(resolved.name) : resolved.name;
186
- const args = resolved.args.map((arg) => format(arg, env, options, table, 999));
187
- return `${name}(${args.join(', ')})`;
197
+ const args = resolved.args.map((arg) => format(arg, env, options, table, 999, 'argument'));
198
+ return `${name}(${args.join(options.compact ? ',' : ', ')})`;
188
199
  }
189
200
 
190
201
  export function formatTermForWrite(term, env = new Env(), options = {}) {
@@ -194,6 +205,8 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
194
205
  numbervars: options.numbervars !== false,
195
206
  doubleQuotes: options.doubleQuotes,
196
207
  variableNames: options.variableNames instanceof Map ? options.variableNames : new Map(),
208
+ compact: options.compact === true,
209
+ operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
197
210
  };
198
211
  return format(term, env, normalized, operatorTable(options.operators), 1200);
199
212
  }
@@ -1,2 +1,2 @@
1
- pair(First, Second)
1
+ pair(First,Second)
2
2
  corrigendum3_write_variable_names.
@@ -523,7 +523,7 @@ c4 ?- call((!;1)).
523
523
  },
524
524
  },
525
525
  {
526
- name: 'occurs_check error mode detects STO while ISO occurs-check unification still fails',
526
+ name: 'occurs_check error mode reports representation_error(term) while ISO occurs-check unification still fails',
527
527
  run: () => {
528
528
  const filename = path.join(tmp, `occurs-check-${++tmpCounter}.pl`);
529
529
  fs.writeFileSync(filename, ':- set_prolog_flag(occurs_check, error).\nsame(X, X).\n');
@@ -544,11 +544,11 @@ c4 ?- call((!;1)).
544
544
  assertEqual(result.stdout,
545
545
  '?- Mode = true.\n' +
546
546
  '?- true.\n' +
547
- '?- error(occurs_check(_A, f(_A))).\n' +
548
- '?- E = error(occurs_check(_A, g(_A)), eyeprolog).\n' +
547
+ '?- error(representation_error(term), []).\n' +
548
+ '?- E = error(representation_error(term), []).\n' +
549
549
  '?- false.\n' +
550
550
  '?- true.\n' +
551
- '?- error(occurs_check(_A, k(_A))).\n' +
551
+ '?- error(representation_error(term), []).\n' +
552
552
  '?- true.\n' +
553
553
  '?- false.\n' +
554
554
  '?- ',
@@ -950,6 +950,23 @@ c4 ?- call((!;1)).
950
950
  assertEqual(run(source, { goal: 'operator_argument(ok)' }).stdout, 'operator_argument(ok).\n', 'operator argument syntax');
951
951
  },
952
952
  },
953
+ {
954
+ name: 'ISO writeq preserves operator atoms in argument syntax',
955
+ run: () => {
956
+ const source = [
957
+ 'emit_operator_arguments :-',
958
+ " writeq([:-,-]), put_char('|'),",
959
+ " writeq(f(*)), put_char('|'),",
960
+ " writeq(f(;,'|',';;')).",
961
+ '',
962
+ ].join('\n');
963
+ assertEqual(
964
+ run(source, { goal: 'emit_operator_arguments' }).stdout,
965
+ "[:-,-]|f(*)|f(;,'|',';;')emit_operator_arguments.\n",
966
+ 'operator argument output',
967
+ );
968
+ },
969
+ },
953
970
  {
954
971
  name: 'ISO query operator and quad infix extension are visible through current_op/3',
955
972
  run: () => {
@@ -1020,7 +1037,7 @@ c4 ?- call((!;1)).
1020
1037
  ].join('\n');
1021
1038
  assertEqual(
1022
1039
  run(source, { goal: 'emit' }).stdout,
1023
- "hello world|'hello world'|a + b * c|'+'(a, *(b, c))|hello world|'hello world'|+(a, b)|a + b|A|$VAR(0)|pair(Left, Right)emit.\n",
1040
+ "hello world|'hello world'|a + b * c|'+'(a,*(b,c))|hello world|'hello world'|+(a,b)|a + b|A|$VAR(0)|pair(Left,Right)emit.\n",
1024
1041
  'stdout',
1025
1042
  );
1026
1043
  },
@@ -785,15 +785,22 @@ subject-to-occurs-check (STO). EyeProlog's default remains the sound finite-tree
785
785
  behavior above. For diagnosis, EyeProlog additionally provides the
786
786
  implementation-specific flag `occurs_check`; setting it to `error` turns a
787
787
  normal unification that would otherwise fail because of the occurs check into
788
- an exception:
788
+ a representation error:
789
789
 
790
790
  ```eyeprolog
791
791
  :- set_prolog_flag(occurs_check, error).
792
792
 
793
793
  sto_example :- X = wrapper(X).
794
- % error(occurs_check(_A, wrapper(_A)))
794
+ % error(representation_error(term), [])
795
795
  ```
796
796
 
797
+ The ISO error mechanism wraps an error term together with an
798
+ implementation-defined context term. For this implementation-specific STO
799
+ diagnostic EyeProlog uses `representation_error(term)` and currently uses the
800
+ empty list `[]` as that context. This reports that the cyclic result of a
801
+ succeeding STO unification cannot be represented by EyeProlog's finite-tree
802
+ term model, without exposing a non-standard `occurs_check/2` error term.
803
+
797
804
  The supported values are `true` (the default finite-tree behavior) and `error`
798
805
  (STO detection). EyeProlog deliberately does not provide `occurs_check=false`,
799
806
  because its term model does not construct cyclic terms. The ISO predicate
@@ -5124,7 +5131,12 @@ parsing of subsequent text, place them before their first use. ISO argument
5124
5131
  syntax also permits an atom that is currently an operator to appear directly
5125
5132
  as a functional argument or list element, so forms such as
5126
5133
  `current_op(Priority, Specifier, :-)` and `[:-,-]` are valid without quoting
5127
- or parenthesizing those operator atoms. The ISO initial operator table also
5134
+ or parenthesizing those operator atoms. Term output observes the same `arg`
5135
+ rule: with `quoted(true)`, an operator atom is not quoted merely because it is
5136
+ an operator when it occurs as a functional argument or list element. Thus
5137
+ `writeq([:-,-])` emits `[:-,-]`, and `writeq(f(;,'|',';;'))` emits
5138
+ `f(;,'|',';;')`; the bar stays quoted because ISO treats the unquoted `|`
5139
+ token as a list separator rather than an atom. The ISO initial operator table also
5128
5140
  contains `?-` at priority 1200 with specifier `fx`, so
5129
5141
  `current_op(1200, fx, ?-)` succeeds. EyeProlog's embedded quad syntax permits
5130
5142
  an optional label before the query marker (`Label ?- Query.`), so while quad
@@ -5164,7 +5176,10 @@ write_event(Path, Event) :-
5164
5176
 
5165
5177
  The period is essential when another Prolog processor will read the result as
5166
5178
  a term. `write/1-2` uses readable conventional syntax, `writeq/1-2` quotes
5167
- where needed, and `write_canonical/1-2` exposes canonical structure.
5179
+ where needed, and `write_canonical/1-2` exposes canonical structure. ISO term
5180
+ output uses only the separator characters needed by the syntax, so functional
5181
+ arguments and list elements are emitted compactly; for example `writeq([a,b])`
5182
+ outputs `[a,b]`.
5168
5183
  `write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
5169
5184
  `variable_names/1`.
5170
5185