eyeprolog 1.3.22 → 1.3.24

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.22",
6
+ "version": "1.3.24",
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
@@ -6,7 +6,7 @@ import {
6
6
  properListItems, termIsGround, termToString, unify, variable, variantTerms,
7
7
  } from './term.js';
8
8
  import { sameNumberValue } from './number-value.js';
9
- import { createParserOperatorState, parseGoalText, parseNumberTokenText, parseTermText } from './parser.js';
9
+ import { NumberRepresentationError, createParserOperatorState, parseGoalText, parseNumberTokenText, parseTermText } from './parser.js';
10
10
  import { formatTermForWrite } from './write.js';
11
11
  import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
12
12
  import {
@@ -1235,7 +1235,8 @@ function readTermFromStream(stream, solver) {
1235
1235
  const term = parseReadTermText(candidate.text, solver);
1236
1236
  stream.position = candidate.end;
1237
1237
  return scopeReadTerm(term);
1238
- } catch (_) {
1238
+ } catch (error) {
1239
+ if (error instanceof NumberRepresentationError) throw new PrologError(error.formal);
1239
1240
  // A dot inside a graphic operator, such as =.., is only a possible
1240
1241
  // terminator. Keep scanning until a complete term parses.
1241
1242
  }
@@ -1371,6 +1372,7 @@ function writeBuiltin(mode) {
1371
1372
  solver.io.writeUnit(stream, formatTermForWrite(goal.args[goal.arity - 1], env, {
1372
1373
  ...options,
1373
1374
  generateVariableNames: true,
1375
+ variableNameState: solver.writeVariableState,
1374
1376
  operators: solver.program.operators.values(),
1375
1377
  }));
1376
1378
  yield env;
@@ -1388,6 +1390,7 @@ function* writeTermBuiltin({ solver, goal, env }) {
1388
1390
  solver.io.writeUnit(stream, formatTermForWrite(goal.args[goal.arity - 2], env, {
1389
1391
  ...options,
1390
1392
  generateVariableNames: true,
1393
+ variableNameState: solver.writeVariableState,
1391
1394
  operators: solver.program.operators.values(),
1392
1395
  }));
1393
1396
  yield env;
@@ -1659,7 +1662,8 @@ function parseIsoNumber(text) {
1659
1662
  const finite = Number(value.name);
1660
1663
  if (!Number.isFinite(finite)) return null;
1661
1664
  return numberTerm(numberTextFromDouble(finite));
1662
- } catch (_) {
1665
+ } catch (error) {
1666
+ if (error instanceof NumberRepresentationError) throw new PrologError(error.formal);
1663
1667
  return null;
1664
1668
  }
1665
1669
  }
@@ -2297,9 +2301,10 @@ function evaluate(term, env) {
2297
2301
  term = deref(term, env);
2298
2302
  if (term.type === VAR) throw new PrologError('instantiation_error');
2299
2303
  if (term.type === NUMBER) {
2300
- return isDecimalInteger(term.name)
2301
- ? { integer: true, value: BigInt(term.name) }
2302
- : { integer: false, value: Number(term.name) };
2304
+ if (isDecimalInteger(term.name)) return { integer: true, value: BigInt(term.name) };
2305
+ const value = Number(term.name);
2306
+ if (!Number.isFinite(value)) throw new PrologError('evaluation_error(float_overflow)');
2307
+ return { integer: false, value };
2303
2308
  }
2304
2309
  if (term.type === ATOM) {
2305
2310
  if (term.name === 'pi') return { integer: false, value: Math.PI };
package/src/parser.js CHANGED
@@ -3,6 +3,23 @@
3
3
  import { ATOM, COMPOUND, atom, compound, cons, emptyList, numberTerm, variable } from './term.js';
4
4
  import { continuesGraphicToken, isTerminatingFullStop } from './syntax-scan.js';
5
5
 
6
+
7
+ export class NumberRepresentationError extends Error {
8
+ constructor(formal) {
9
+ super(`error(${formal})`);
10
+ this.name = 'NumberRepresentationError';
11
+ this.formal = formal;
12
+ }
13
+ }
14
+
15
+ function finiteFloatTokenText(text) {
16
+ const value = Number(text);
17
+ if (!Number.isFinite(value)) {
18
+ throw new NumberRepresentationError('representation_error(max_float)');
19
+ }
20
+ return value === 0 ? '0.0' : text;
21
+ }
22
+
6
23
  const TOK = {
7
24
  EOF: 'eof', ATOM: 'atom', VAR: 'var', STRING: 'string', NUMBER: 'number',
8
25
  LPAREN: '(', RPAREN: ')', LBRACKET: '[', RBRACKET: ']', LBRACE: '{', RBRACE: '}',
@@ -536,7 +553,7 @@ class Parser {
536
553
  }
537
554
  let text = this.source.slice(start, this.pos);
538
555
  if (!hasFraction) text = BigInt(text).toString();
539
- else if (Object.is(Number(text), -0)) text = '0.0';
556
+ else text = finiteFloatTokenText(text);
540
557
  return { type: TOK.NUMBER, text, line };
541
558
  }
542
559
 
@@ -1510,8 +1527,7 @@ export function parseNumberTokenText(text) {
1510
1527
  }
1511
1528
  if (position !== source.length) throw invalidNumberTokenError;
1512
1529
  if (/^-?\d+$/.test(source)) return numberTerm(BigInt(source).toString());
1513
- if (Object.is(Number(source), -0)) return numberTerm('0.0');
1514
- return numberTerm(source);
1530
+ return numberTerm(finiteFloatTokenText(source));
1515
1531
  }
1516
1532
 
1517
1533
  export function parseTermText(text, options = {}) {
package/src/solver.js CHANGED
@@ -116,6 +116,11 @@ export class Solver {
116
116
  }
117
117
  }
118
118
  this.io = options.io ?? new StreamManager(options.ioOptions);
119
+ // Keep generated write-variable names stable for the lifetime of one
120
+ // top-level query. Inner/meta-call solvers share this state so separate
121
+ // write/1, writeq/1, write_canonical/1, and write_term/2-3 calls can refer
122
+ // to the same logical variable by the same printed name.
123
+ this.writeVariableState = options.writeVariableState ?? { depth: 0, names: new Map(), next: 0 };
119
124
  this.solveStacks = [];
120
125
  this.active = [];
121
126
  this.cutEpoch = 0;
@@ -163,6 +168,7 @@ export class Solver {
163
168
  io: this.io,
164
169
  innerTableScopes: this.innerTableScopes,
165
170
  inferenceObservation: this.inferenceObservation,
171
+ writeVariableState: this.writeVariableState,
166
172
  skipListTailTabling: options.skipListTailTabling ?? this.skipListTailTabling,
167
173
  });
168
174
  if (options.tableScope != null) {
@@ -279,6 +285,13 @@ export class Solver {
279
285
  if (!Array.isArray(goals)) goals = [goals];
280
286
  env.setOccursCheckHandler(this.occursCheckHandler);
281
287
 
288
+ const writeVariableState = this.writeVariableState;
289
+ if (writeVariableState.depth === 0) {
290
+ writeVariableState.names.clear();
291
+ writeVariableState.next = 0;
292
+ }
293
+ writeVariableState.depth++;
294
+
282
295
  const savedActive = this.active;
283
296
  let registeredStack = null;
284
297
  try {
@@ -651,6 +664,11 @@ export class Solver {
651
664
  const stackIndex = this.solveStacks.indexOf(registeredStack);
652
665
  if (stackIndex >= 0) this.solveStacks.splice(stackIndex, 1);
653
666
  this.active = savedActive;
667
+ writeVariableState.depth = Math.max(0, writeVariableState.depth - 1);
668
+ if (writeVariableState.depth === 0) {
669
+ writeVariableState.names.clear();
670
+ writeVariableState.next = 0;
671
+ }
654
672
  }
655
673
  }
656
674
 
package/src/write.js CHANGED
@@ -180,13 +180,14 @@ function generatedVariableName(index) {
180
180
  return suffix === 0 ? `_${letter}` : `_${letter}${suffix}`;
181
181
  }
182
182
 
183
- function printableGeneratedVariableNames(term, env, explicit) {
183
+ function printableGeneratedVariableNames(term, env, explicit, state = null) {
184
184
  const names = new Map(explicit);
185
- const used = new Set(names.values());
185
+ const sharedNames = state?.names instanceof Map ? state.names : new Map();
186
+ const used = new Set([...sharedNames.values(), ...names.values()]);
186
187
  const seenVariables = new Set();
187
188
  const seenTerms = new Set();
188
189
  const stack = [term];
189
- let generated = 0;
190
+ let generated = Number.isSafeInteger(state?.next) ? state.next : 0;
190
191
 
191
192
  while (stack.length) {
192
193
  const current = deref(stack.pop(), env);
@@ -194,8 +195,14 @@ function printableGeneratedVariableNames(term, env, explicit) {
194
195
  if (seenVariables.has(current.name)) continue;
195
196
  seenVariables.add(current.name);
196
197
  if (names.has(current.name)) continue;
198
+ const shared = sharedNames.get(current.name);
199
+ if (shared != null) {
200
+ names.set(current.name, shared);
201
+ continue;
202
+ }
197
203
  let candidate;
198
204
  do candidate = generatedVariableName(generated++); while (used.has(candidate));
205
+ sharedNames.set(current.name, candidate);
199
206
  names.set(current.name, candidate);
200
207
  used.add(candidate);
201
208
  continue;
@@ -205,6 +212,7 @@ function printableGeneratedVariableNames(term, env, explicit) {
205
212
  for (let i = current.arity - 1; i >= 0; i--) stack.push(current.args[i]);
206
213
  }
207
214
 
215
+ if (state != null) state.next = generated;
208
216
  return names;
209
217
  }
210
218
 
@@ -317,7 +325,7 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
317
325
  numbervars: options.numbervars !== false,
318
326
  doubleQuotes: options.doubleQuotes,
319
327
  variableNames: options.generateVariableNames === true
320
- ? printableGeneratedVariableNames(term, env, explicitVariableNames)
328
+ ? printableGeneratedVariableNames(term, env, explicitVariableNames, options.variableNameState)
321
329
  : printableReadVariableNames(term, env, explicitVariableNames),
322
330
  compact: options.compact === true,
323
331
  operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
@@ -738,6 +738,77 @@ c4 ?- call((!;1)).
738
738
  assertEqual(result.stderr, '', 'stderr');
739
739
  },
740
740
  },
741
+ {
742
+ name: 'float literals reject overflow and normalize underflow (issue #54)',
743
+ run: () => {
744
+ const result = runCli([], {
745
+ input: [
746
+ 'T = 1.0e-99999, F is T.',
747
+ 'T = 1.0e99999, float(T).',
748
+ 'T = 1.0e99999, float(T), F is T.',
749
+ 'T = 1.0e99999, U = 2.0e99999, T > U.',
750
+ 'T = 1.0e99999, U = 2.0e99999, T < U.',
751
+ 'T = 1.0e99999, U = 2.0e99999, T = U.',
752
+ 'T = 1.0e99999, U = 2.0e99999, T =:= U.',
753
+ 'halt.',
754
+ '',
755
+ ].join('\n'),
756
+ });
757
+ assertEqual(result.status, 0, 'exit status');
758
+ assertIncludes(result.stdout, 'T = 0.0, F = 0.0.', 'underflow rounds on input');
759
+ assertEqual(
760
+ (result.stdout.match(/error\(representation_error\(max_float\)\)\./g) ?? []).length,
761
+ 6,
762
+ 'all overflowing literals fail while being read',
763
+ );
764
+ assertEqual(result.stderr, '', 'stderr');
765
+
766
+ let overflow = null;
767
+ try {
768
+ parseNumberTokenText('1.0e99999');
769
+ } catch (error) {
770
+ overflow = error;
771
+ }
772
+ assertEqual(overflow?.formal, 'representation_error(max_float)', 'number token overflow');
773
+ assertEqual(parseNumberTokenText('1.0e-99999').name, '0.0', 'number token underflow');
774
+
775
+ const chars = Array.from('1.0e99999', atom);
776
+ const numberChars = createDefaultRegistry().get('number_chars', 2).handler;
777
+ let numberCharsError = null;
778
+ try {
779
+ numberChars({
780
+ goal: compound('number_chars', [variable('N'), listFromItems(chars)]),
781
+ env: new Env(),
782
+ }).next();
783
+ } catch (error) {
784
+ numberCharsError = error;
785
+ }
786
+ assertEqual(numberCharsError?.formal, 'representation_error(max_float)', 'number_chars overflow');
787
+
788
+ let readError = null;
789
+ try {
790
+ runEyeProlog('', {
791
+ goal: 'read(X)',
792
+ ioOptions: { input: '1.0e99999.\n' },
793
+ });
794
+ } catch (error) {
795
+ readError = error;
796
+ }
797
+ assertEqual(readError?.formal, 'representation_error(max_float)', 'read/1 overflow');
798
+
799
+ const isHandler = createDefaultRegistry().get('is', 2).handler;
800
+ let hostTermError = null;
801
+ try {
802
+ isHandler({
803
+ goal: compound('is', [variable('F'), numberTerm('1.0e99999')]),
804
+ env: new Env(),
805
+ }).next();
806
+ } catch (error) {
807
+ hostTermError = error;
808
+ }
809
+ assertEqual(hostTermError?.formal, 'evaluation_error(float_overflow)', 'host-created non-finite term');
810
+ },
811
+ },
741
812
  {
742
813
  name: 'readers keep a full stop inside a character-code constant (WG17 #367)',
743
814
  run: () => {
@@ -2989,6 +3060,20 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
2989
3060
  assertEqual(fresh.stdout, '_A\nemit.\n', 'fresh clause variable hides its internal suffix');
2990
3061
  },
2991
3062
  },
3063
+ {
3064
+ name: 'write predicates keep generated variable names stable across calls (issue #53 comment 5356861151)',
3065
+ run: () => {
3066
+ const result = run([
3067
+ "emit :- write_term(pair(A,B), []), write(' / '), write_term(user_output,B,[]), write(' / '), writeq(A), write(' / '), write_canonical(B), nl.",
3068
+ "again :- write_canonical(B+B), nl.",
3069
+ ].join('\n'), { goals: ['emit', 'again'] });
3070
+ assertEqual(
3071
+ result.stdout,
3072
+ 'pair(_A,_B) / _B / _A / _B\nemit.\n+(_A,_A)\nagain.\n',
3073
+ 'stable names across calls and reset at the next top-level query',
3074
+ );
3075
+ },
3076
+ },
2992
3077
  {
2993
3078
  name: 'write predicates and write_term options select distinct formats',
2994
3079
  run: () => {