eyeprolog 1.3.82 → 1.3.83

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.3.82",
6
+ "version": "1.3.83",
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
@@ -804,18 +804,7 @@ function* currentPrologFlagBuiltin({ solver, goal, env }) {
804
804
  }
805
805
 
806
806
  function* setPrologFlagBuiltin({ solver, goal, env }) {
807
- const flag = deref(goal.args[0], env);
808
- const value = deref(goal.args[1], env);
809
- if (flag.type === VAR || value.type === VAR) throw new PrologError('instantiation_error');
810
- if (flag.type !== ATOM) throw new PrologError('type_error(atom)', flag);
811
- const definition = solver.prologFlags.get(flag.name);
812
- if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
813
- const expectedType = definition.valueType ?? ATOM;
814
- if (value.type !== expectedType || !definition.allowed.includes(value.name)) {
815
- throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
816
- }
817
- if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
818
- definition.value = atom(value.name);
807
+ solver.setPrologFlag(goal.args[0], goal.args[1], env);
819
808
  yield env;
820
809
  }
821
810
 
@@ -1720,6 +1709,15 @@ function writeOptionBoolean(value, env, option) {
1720
1709
  return value.name === 'true';
1721
1710
  }
1722
1711
 
1712
+ function writeOptionSpacing(value, env, option) {
1713
+ value = deref(value, env);
1714
+ if (value.type === VAR) throw new PrologError('instantiation_error');
1715
+ if (value.type !== ATOM || !['standard', 'minimal'].includes(value.name)) {
1716
+ throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1717
+ }
1718
+ return value.name;
1719
+ }
1720
+
1723
1721
  function writeVariableNames(value, env, option) {
1724
1722
  value = deref(value, env);
1725
1723
  if (value.type === VAR) throw new PrologError('instantiation_error');
@@ -1748,8 +1746,10 @@ function writeVariableNames(value, env, option) {
1748
1746
  return names;
1749
1747
  }
1750
1748
 
1751
- function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = null) {
1752
- const result = defaultTermWriteOptions(mode);
1749
+ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = null, initial = null) {
1750
+ const result = initial == null
1751
+ ? defaultTermWriteOptions(mode)
1752
+ : { ...initial, variableNames: new Map(initial.variableNames ?? []) };
1753
1753
  for (const option of options) {
1754
1754
  if (option.type !== COMPOUND || option.arity !== 1) {
1755
1755
  throw new PrologError('domain_error(write_option)', copyResolved(option, env));
@@ -1760,6 +1760,8 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
1760
1760
  else if (option.name === 'variable_names') result.variableNames = writeVariableNames(option.args[0], env, option);
1761
1761
  else if (option.name === 'double_quotes' && !solver?.isoStrict) {
1762
1762
  result.doubleQuotes = writeOptionBoolean(option.args[0], env, option);
1763
+ } else if (option.name === 'spacing' && !solver?.isoStrict) {
1764
+ result.minimalOperatorSpacing = writeOptionSpacing(option.args[0], env, option) === 'minimal';
1763
1765
  } else {
1764
1766
  throw new PrologError('domain_error(write_option)', copyResolved(option, env));
1765
1767
  }
@@ -1767,6 +1769,12 @@ function termWriteOptionsFromItems(options, env, mode = 'write_term', solver = n
1767
1769
  return result;
1768
1770
  }
1769
1771
 
1772
+ export function parseTermWriteOptions(optionTerm, env, mode = 'write_term', solver = null, initial = null) {
1773
+ const preflight = preflightOptionList(optionTerm, env);
1774
+ const items = requireProperOptionList(preflight);
1775
+ return termWriteOptionsFromItems(items, env, mode, solver, initial);
1776
+ }
1777
+
1770
1778
 
1771
1779
  function writeBuiltin(mode) {
1772
1780
  return function* ({ solver, goal, env }) {
package/src/repl.js CHANGED
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import { readSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
- import { formalErrorTerm } from './iso.js';
6
+ import { formalErrorTerm, parseTermWriteOptions } from './iso.js';
7
7
  import {
8
8
  characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
9
9
  } from './syntax-scan.js';
@@ -303,20 +303,18 @@ function makeState(engine, sources, output, options = {}, previousState = null,
303
303
  const flagOverrides = new Map(previousState?.flagOverrides ?? []);
304
304
  for (const [name, value] of flagOverrides) {
305
305
  const definition = solver.prologFlags.get(name);
306
- if (definition?.changeable && definition.allowed.includes(value)) {
307
- definition.value = engine.atom(value);
308
- }
306
+ if (definition?.changeable) definition.value = value;
309
307
  }
310
308
  return { program: solver.program, solver, strictIso, flagOverrides };
311
309
  }
312
310
 
313
311
  function snapshotFlagValues(solver) {
314
- return new Map([...solver.prologFlags].map(([name, definition]) => [name, definition.value?.name]));
312
+ return new Map([...solver.prologFlags].map(([name, definition]) => [name, definition.value]));
315
313
  }
316
314
 
317
315
  function rememberFlagOverrides(state, before) {
318
316
  for (const [name, definition] of state.solver.prologFlags) {
319
- const value = definition.value?.name;
317
+ const value = definition.value;
320
318
  if (before.get(name) !== value) state.flagOverrides.set(name, value);
321
319
  }
322
320
  }
@@ -720,6 +718,7 @@ function formatAnswer(engine, state, variables, env) {
720
718
  const valueMaxPriority = equality == null
721
719
  ? 699
722
720
  : equality.specifier === 'xfy' ? equality.priority : equality.priority - 1;
721
+ const writeOptions = answerTermWriteOptions(state, env);
723
722
  let generated = 0;
724
723
 
725
724
  // Meta-predicate wrappers can make a query variable alias an internal fresh
@@ -734,7 +733,7 @@ function formatAnswer(engine, state, variables, env) {
734
733
  if (value.type === 'var' &&
735
734
  (value.name === variable.name || !queryVariableNames.has(value.name))) continue;
736
735
  bindings.push(`${variable.name} = ${engine.formatTermForWrite(value, env, {
737
- quoted: true,
736
+ ...writeOptions,
738
737
  operators,
739
738
  variableNames: names,
740
739
  maxPriority: valueMaxPriority,
@@ -743,7 +742,6 @@ function formatAnswer(engine, state, variables, env) {
743
742
  // quotes, just as writeq/1 already prints them.
744
743
  operatorAtomsAsArgs: true,
745
744
  dottedGraphicAtoms: true,
746
- doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
747
745
  })}`);
748
746
  }
749
747
  for (const constraint of env.variableConstraints?.() ?? []) {
@@ -751,12 +749,11 @@ function formatAnswer(engine, state, variables, env) {
751
749
  if (residual == null) continue;
752
750
  collectUnboundVariables(engine, residual, env, names, () => `_${letterName(generated++)}`);
753
751
  bindings.push(engine.formatTermForWrite(residual, env, {
754
- quoted: true,
752
+ ...writeOptions,
755
753
  operators,
756
754
  variableNames: names,
757
755
  operatorAtomsAsArgs: true,
758
756
  dottedGraphicAtoms: true,
759
- doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
760
757
  }));
761
758
  }
762
759
  const projectedAttributeRoots = new Set();
@@ -768,18 +765,37 @@ function formatAnswer(engine, state, variables, env) {
768
765
  for (const residual of state.solver.attributeResidualGoals(root, env)) {
769
766
  collectUnboundVariables(engine, residual, env, names, () => `_${letterName(generated++)}`);
770
767
  bindings.push(engine.formatTermForWrite(residual, env, {
771
- quoted: true,
768
+ ...writeOptions,
772
769
  operators,
773
770
  variableNames: names,
774
771
  operatorAtomsAsArgs: true,
775
772
  dottedGraphicAtoms: true,
776
- doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
777
773
  }));
778
774
  }
779
775
  }
780
776
  return bindings.length === 0 ? 'true' : bindings.join(', ');
781
777
  }
782
778
 
779
+ function answerTermWriteOptions(state, env) {
780
+ const doubleQuotes = state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars';
781
+ const initial = {
782
+ quoted: true,
783
+ ignoreOps: false,
784
+ numbervars: true,
785
+ variableNames: new Map(),
786
+ compact: false,
787
+ minimalOperatorSpacing: false,
788
+ operatorAtomsAsArgs: true,
789
+ doubleQuotes,
790
+ };
791
+ const optionTerm = state.solver.prologFlags.get('answer_write_options')?.value;
792
+ if (optionTerm == null) return initial;
793
+ const options = parseTermWriteOptions(optionTerm, env, 'write_term', state.solver, initial);
794
+ if (options.doubleQuotes === true) options.doubleQuotes = doubleQuotes;
795
+ else if (options.doubleQuotes === false) options.doubleQuotes = null;
796
+ return options;
797
+ }
798
+
783
799
  function collectUnboundVariables(engine, term, env, names, nextName) {
784
800
  const stack = [term];
785
801
  const seen = new Set();
package/src/solver.js CHANGED
@@ -1,12 +1,12 @@
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
- ATOM, COMPOUND, NUMBER, STRING, VAR, Env, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
4
+ ATOM, COMPOUND, NUMBER, STRING, VAR, Env, atom, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
5
5
  flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
6
- numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
6
+ listFromItems, numberTerm, numberTextFromDouble, properListItems, termIsGround, termToString, unify, variable, variantTerms,
7
7
  } from './term.js';
8
8
  import { numberValueKey, sameNumberValue } from './number-value.js';
9
- import { PrologError, getStrictIsoRegistry } from './iso.js';
9
+ import { PrologError, getStrictIsoRegistry, parseTermWriteOptions } from './iso.js';
10
10
  import { getEyePrologRegistry } from './standard-library.js';
11
11
  import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
12
12
  import { StreamManager } from './io.js';
@@ -144,15 +144,7 @@ export class Solver {
144
144
  this.prologFlags.get('double_quotes').value = compound(program.doubleQuotes, []);
145
145
  }
146
146
  for (const [flag, value] of program.prologFlagDirectives ?? []) {
147
- if (flag.type === 'var' || value.type === 'var') throw new PrologError('instantiation_error');
148
- if (flag.type !== 'atom') throw new PrologError('type_error(atom)', flag);
149
- const definition = this.prologFlags.get(flag.name);
150
- if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
151
- if (value.type !== 'atom' || !definition.allowed.includes(value.name)) {
152
- throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
153
- }
154
- if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
155
- definition.value = value;
147
+ this.setPrologFlag(flag, value, new Env());
156
148
  }
157
149
  }
158
150
  if (!options.charConversions) {
@@ -206,6 +198,35 @@ export class Solver {
206
198
  };
207
199
  }
208
200
 
201
+ setPrologFlag(flagTerm, valueTerm, env = new Env()) {
202
+ const flag = deref(flagTerm, env);
203
+ const value = deref(valueTerm, env);
204
+ if (flag.type === VAR || value.type === VAR) throw new PrologError('instantiation_error');
205
+ if (flag.type !== ATOM) throw new PrologError('type_error(atom)', flag);
206
+ const definition = this.prologFlags.get(flag.name);
207
+ if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
208
+
209
+ if (definition.writeOptions === true) {
210
+ if (!termIsGround(value, env)) throw new PrologError('instantiation_error');
211
+ try {
212
+ parseTermWriteOptions(value, env, 'write_term', this);
213
+ } catch (error) {
214
+ if (error instanceof PrologError && error.formal === 'instantiation_error') throw error;
215
+ throw new PrologError('domain_error(flag_value)', compound('+', [flag, copyResolved(value, env)]));
216
+ }
217
+ if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
218
+ definition.value = copyResolved(value, env);
219
+ return;
220
+ }
221
+
222
+ const expectedType = definition.valueType ?? ATOM;
223
+ if (value.type !== expectedType || !definition.allowed.includes(value.name)) {
224
+ throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
225
+ }
226
+ if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
227
+ definition.value = atom(value.name);
228
+ }
229
+
209
230
  cloneForInnerGoal(solutionLimit = this.solutionLimit, options = {}) {
210
231
  const solver = new Solver(this.program, {
211
232
  registry: this.registry,
@@ -1072,6 +1093,12 @@ function defaultPrologFlags(unknown = 'error', strictIso = false) {
1072
1093
  ['unknown', { value: compound(unknown, []), allowed: ['error', 'fail', 'warning'], changeable: true }],
1073
1094
  ['double_quotes', { value: compound('chars', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
1074
1095
  ['occurs_check', { value: compound('true', []), allowed: ['true', 'error'], changeable: true }],
1096
+ ['answer_write_options', {
1097
+ value: listFromItems([compound('spacing', [compound('standard', [])])]),
1098
+ allowed: [],
1099
+ changeable: true,
1100
+ writeOptions: true,
1101
+ }],
1075
1102
  ]);
1076
1103
  if (strictIso) flags.delete('occurs_check');
1077
1104
  return flags;
@@ -97,8 +97,8 @@ required permission error. `read_term/3`, `write_term/3`, `op/3`, and
97
97
  EyeProlog's chosen simultaneous-error behavior stable. Section 7.12 makes that
98
98
  choice implementation dependent when several error conditions hold at once. Strict `write_term/2-3` accepts only
99
99
  the Part 1 plus Corrigendum 3 option surface; the normal-profile
100
- `double_quotes/1` write option remains an explicitly documented EyeProlog
101
- extension.
100
+ `double_quotes/1` and `spacing/1` write options remain explicitly documented
101
+ EyeProlog extensions.
102
102
 
103
103
 
104
104
  The preparation, stream, conversion, sorting, and arithmetic audit expands the coverage into Prolog-text,
@@ -120,11 +120,11 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
120
120
  | 5.5.5 Directives | `module/2`, `use_module/1-2`, `meta_predicate/1` and normal-profile library behavior | Rejected as implementation-specific Part 1 directives. |
121
121
  | 5.5.6 Side effects | Normal mode adds `statistics/0-2`, cleanup/library state, and optional proof/statistics host instrumentation. | The strict registry excludes Prolog-visible statistics/cleanup/library adapters; ordinary Part 1 I/O/database/flag/operator side effects remain. Host proof/statistics collection observes execution through the embedding API rather than adding a strict Prolog goal effect. **covered** — strict registry/execution regression. |
122
122
  | 5.5.7 Control constructs | `tnot/1` and normal-profile execution optimizations | `tnot/1` is absent; automatic tabling/recursion guards are disabled. |
123
- | 5.5.8 Flags | `occurs_check` | Absent in strict mode. |
123
+ | 5.5.8 Flags | `occurs_check`; `answer_write_options`, whose normal-mode default is `[spacing(standard)]` and whose value controls REPL answer rendering | Both extension flags are absent in strict mode. |
124
124
  | 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and interop autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
125
125
  | 5.5.10 Evaluable functors | Normal mode additionally accepts the EyeProlog evaluable atom `e`; the remaining arithmetic functors accepted by strict mode are the Part 1 + Corrigenda set. | Strict mode rejects `e/0` as non-evaluable and retains the Corrigendum arithmetic additions. **covered** — strict extension-boundary regression plus `src/iso-arithmetic.js`. |
126
126
  | 5.5.11 Reserved atoms | None | None. |
127
- | Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` option `double_quotes(true|false)`. Strict core excludes that extension and accepts only the Part 1 plus Corrigendum 3 write-option surface; unknown extension options raise `domain_error(write_option,...)`. |
127
+ | Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Normal mode additionally accepts the EyeProlog `write_term/2-3` options `double_quotes(true|false)` and `spacing(standard|minimal)`. Strict core excludes those extensions and accepts only the Part 1 plus Corrigendum 3 write-option surface; unknown extension options raise `domain_error(write_option,...)`. |
128
128
 
129
129
  Normal mode provides documented module and DCG compatibility profiles whose
130
130
  features overlap standardized Part 2 and Part 3 facilities. They are extensions
@@ -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_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)]).
3
+ flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true), pair(answer_write_options, [spacing(standard)])]).
@@ -41,6 +41,7 @@ export function runIsoStrict(reporter = new TestReporter()) {
41
41
  reporter.test('exposes only ISO core Prolog flags', () => {
42
42
  const solver = new Solver(Program.parse('', { isoStrict: true }), { isoStrict: true });
43
43
  equal(solver.prologFlags.has('occurs_check'), false, 'occurs_check');
44
+ equal(solver.prologFlags.has('answer_write_options'), false, 'answer_write_options');
44
45
  equal(solver.prologFlags.get('unknown')?.value?.name, 'error', 'unknown default');
45
46
  });
46
47
 
@@ -283,8 +284,16 @@ export function runIsoStrict(reporter = new TestReporter()) {
283
284
  }));
284
285
  equal(strictError.formal, 'domain_error(write_option)', 'strict extension rejection');
285
286
 
287
+ const strictSpacingError = capture(() => run('', {
288
+ isoStrict: true,
289
+ goal: 'write_term(1*1,[spacing(minimal)])',
290
+ }));
291
+ equal(strictSpacingError.formal, 'domain_error(write_option)', 'strict spacing extension rejection');
292
+
286
293
  const normal = run('', { goal: 'write_term("ab",[double_quotes(true)])' });
287
294
  includes(normal.stdout, '"ab"', 'normal-profile extension remains available');
295
+ includes(run('', { goal: 'write_term(1*1,[spacing(standard)])' }).stdout,
296
+ '1 * 1', 'normal-profile spacing extension remains available');
288
297
  });
289
298
 
290
299
  reporter.test('follows Corrigendum 3 variable metadata traversal and write naming', () => {
@@ -3881,6 +3881,70 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
3881
3881
  assertEqual(run(source, { goal: 'emit' }).stdout, '1+2|a+ -b|a+b*c\nemit.\n', 'minimal operator spacing');
3882
3882
  },
3883
3883
  },
3884
+ {
3885
+ name: 'answer write options make operator spacing explicit and persistent (issue #76)',
3886
+ run: () => {
3887
+ const source = [
3888
+ 'emit :-',
3889
+ ' write_term(1*1, [spacing(standard)]), put_char(\'|\'),',
3890
+ ' write_term(1*1, [spacing(minimal)]), put_char(\'|\'),',
3891
+ ' write_term(1*1, [spacing(standard),spacing(minimal)]).',
3892
+ '',
3893
+ ].join('\n');
3894
+ assertEqual(run(source, { goal: 'emit' }).stdout, '1 * 1|1*1|1*1emit.\n',
3895
+ 'explicit and rightmost spacing options');
3896
+ assertEqual(run([
3897
+ ':- set_prolog_flag(answer_write_options, [spacing(minimal)]).',
3898
+ 'emit :- current_prolog_flag(answer_write_options, [spacing(minimal)]).',
3899
+ ], { goal: 'emit' }).stdout, 'emit.\n', 'source flag directive');
3900
+
3901
+ const result = runCli([], {
3902
+ input:
3903
+ 'current_prolog_flag(answer_write_options, Os).\n' +
3904
+ 'X=1*1.\n' +
3905
+ 'set_prolog_flag(answer_write_options,[spacing(minimal)]).\n' +
3906
+ 'use_module(library(lists)).\n' +
3907
+ 'X=1*1.\n' +
3908
+ 'current_prolog_flag(answer_write_options, Os).\n' +
3909
+ 'halt.\n',
3910
+ });
3911
+ assertEqual(result.status, 0, 'exit status');
3912
+ assertEqual(result.stdout,
3913
+ '?- Os = [spacing(standard)].\n' +
3914
+ '?- X = 1 * 1.\n' +
3915
+ '?- true.\n' +
3916
+ '?- true.\n' +
3917
+ '?- X = 1*1.\n' +
3918
+ '?- Os = [spacing(minimal)].\n' +
3919
+ '?- ',
3920
+ 'discoverable top-level option and persistence across module reload');
3921
+ assertEqual(result.stderr, '', 'stderr');
3922
+
3923
+ let writeOptionError = null;
3924
+ try {
3925
+ run('', { goal: 'write_term(a,[spacing(other)])' });
3926
+ } catch (error) {
3927
+ writeOptionError = error;
3928
+ }
3929
+ assertEqual(writeOptionError?.formal, 'domain_error(write_option)', 'spacing value domain');
3930
+
3931
+ let flagValueError = null;
3932
+ try {
3933
+ run('', { goal: 'set_prolog_flag(answer_write_options,[spacing(other)])' });
3934
+ } catch (error) {
3935
+ flagValueError = error;
3936
+ }
3937
+ assertEqual(flagValueError?.formal, 'domain_error(flag_value)', 'answer option flag domain');
3938
+
3939
+ let nongroundFlagError = null;
3940
+ try {
3941
+ run('', { goal: 'set_prolog_flag(answer_write_options,[spacing(X)])' });
3942
+ } catch (error) {
3943
+ nongroundFlagError = error;
3944
+ }
3945
+ assertEqual(nongroundFlagError?.formal, 'instantiation_error', 'answer options must be ground');
3946
+ },
3947
+ },
3884
3948
  {
3885
3949
  name: 'write predicates and write_term options select distinct formats',
3886
3950
  run: () => {
@@ -5426,8 +5426,10 @@ still retained where adjacent graphic tokens would otherwise merge, as in
5426
5426
  `write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
5427
5427
  `variable_names/1`. Normal mode additionally accepts the EyeProlog extension
5428
5428
  `double_quotes(true|false)`: `true` lets eligible character/code lists use the
5429
- current `double_quotes` representation, while strict ISO mode rejects this
5430
- non-standard write option.
5429
+ current `double_quotes` representation. It also accepts
5430
+ `spacing(standard|minimal)`: `standard` places layout around operators, while
5431
+ `minimal` emits only the separators required to avoid lexical ambiguity.
5432
+ Strict ISO mode rejects both non-standard write options.
5431
5433
 
5432
5434
  Character operations are `get_char`, `peek_char`, `put_char`, `get_code`,
5433
5435
  `peek_code`, and `put_code`; byte streams use the corresponding byte
@@ -6000,7 +6002,7 @@ silently changing a static program.
6000
6002
  | `char_conversion(+Input,+Output)` | Installs a one-character conversion. In prepared Prolog text, later **unquoted** characters are converted when `char_conversion=on`; quoted characters are unchanged. The same mapping initializes execution-time term input. Mapping a character to itself removes its custom mapping. |
6001
6003
  | `current_char_conversion(?Input,?Output)` | Enumerates installed nonidentity conversions. |
6002
6004
  | `current_prolog_flag(?Flag,?Value)` | Enumerates flags or retrieves one named flag. An unknown bound flag raises *domain_error(prolog_flag)*. |
6003
- | `set_prolog_flag(+Flag,+Value)` | Changes a supported mutable flag after validating its allowed atom value. Read-only flags raise a permission error. |
6005
+ | `set_prolog_flag(+Flag,+Value)` | Changes a supported mutable flag after validating its value. Read-only flags raise a permission error. |
6004
6006
 
6005
6007
  | Flag | Default in normal EyeProlog | Allowed values | Mutable |
6006
6008
  | --- | --- | --- | --- |
@@ -6014,6 +6016,7 @@ silently changing a static program.
6014
6016
  | `unknown` | `error` | `error`, `fail`, `warning` | yes |
6015
6017
  | `double_quotes` | `chars` | `chars`, `codes`, `atom` | yes |
6016
6018
  | `occurs_check` | `true` | `true`, `error` | yes |
6019
+ | `answer_write_options` | `[spacing(standard)]` | A ground proper list of supported normal-mode `write_term/2-3` options | yes |
6017
6020
 
6018
6021
  Because `bounded=false`, `current_prolog_flag(max_integer, _)` and
6019
6022
  `current_prolog_flag(min_integer, _)` fail as required by ISO 7.11.1.1;
@@ -6048,6 +6051,24 @@ codes("ab"). % codes([97,98])
6048
6051
  quoted_atom("ab"). % quoted_atom(ab)
6049
6052
  ```
6050
6053
 
6054
+ Normal mode also exposes the REPL's term-writing options through the mutable
6055
+ `answer_write_options` flag. Its explicit default explains the spaces in an
6056
+ answer such as `X = 1 * 1`; selecting minimal spacing is discoverable and
6057
+ reusable from Prolog:
6058
+
6059
+ ```text
6060
+ ?- current_prolog_flag(answer_write_options, Options).
6061
+ Options = [spacing(standard)].
6062
+ ?- set_prolog_flag(answer_write_options, [spacing(minimal)]).
6063
+ true.
6064
+ ?- X = 1*1.
6065
+ X = 1*1.
6066
+ ```
6067
+
6068
+ The setting survives interactive `consult/1` and `use_module/1-2` program
6069
+ rebuilds. `answer_write_options` is a normal-mode host flag and is absent in
6070
+ strict ISO mode.
6071
+
6051
6072
  ### Atomic-term operations and conversions
6052
6073
 
6053
6074
  | Predicate and principal call | Behavior |
@@ -6120,7 +6141,7 @@ the beginning. A peek does not mark the stream as past-end.
6120
6141
  | `write(+Term)`, `write(+Stream,+Term)` | Writes readable operator notation without quoting atoms merely because quoting would be required for reparsing. Number variables are enabled. |
6121
6142
  | `writeq(+Term)`, `writeq(+Stream,+Term)` | Like `write`, but quotes atoms when required for unambiguous input syntax. |
6122
6143
  | `write_canonical(+Term)`, `write_canonical(+Stream,+Term)` | Writes quoted canonical functor notation while ignoring operators and without interpreting *$VAR/1*. |
6123
- | `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. |
6144
+ | `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. Normal mode also supports *double_quotes(true or false)* and *spacing(standard or minimal)*. |
6124
6145
 
6125
6146
  Term input uses the program's current operator table and the same ISO quoted-character
6126
6147
  syntax as source text, including backslash-terminated octal and hexadecimal
@@ -7765,8 +7786,8 @@ Notable implementation boundaries are:
7765
7786
  - `write_term/2-3` implements the Part 1 plus Corrigendum 3 `quoted/1`,
7766
7787
  `ignore_ops/1`, `numbervars/1`, and `variable_names/1` option surface,
7767
7788
  including option validation and traversal rules; normal mode also offers
7768
- `double_quotes(true|false)` as an explicitly non-standard extension, which
7769
- strict mode rejects;
7789
+ `double_quotes(true|false)` and `spacing(standard|minimal)` as explicitly
7790
+ non-standard extensions, which strict mode rejects;
7770
7791
  - unification consistently performs an occurs check, rejecting rational-tree
7771
7792
  bindings accepted as extensions by some systems.
7772
7793