eyeprolog 1.3.23 → 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.23",
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
  }
@@ -1661,7 +1662,8 @@ function parseIsoNumber(text) {
1661
1662
  const finite = Number(value.name);
1662
1663
  if (!Number.isFinite(finite)) return null;
1663
1664
  return numberTerm(numberTextFromDouble(finite));
1664
- } catch (_) {
1665
+ } catch (error) {
1666
+ if (error instanceof NumberRepresentationError) throw new PrologError(error.formal);
1665
1667
  return null;
1666
1668
  }
1667
1669
  }
@@ -2299,9 +2301,10 @@ function evaluate(term, env) {
2299
2301
  term = deref(term, env);
2300
2302
  if (term.type === VAR) throw new PrologError('instantiation_error');
2301
2303
  if (term.type === NUMBER) {
2302
- return isDecimalInteger(term.name)
2303
- ? { integer: true, value: BigInt(term.name) }
2304
- : { 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 };
2305
2308
  }
2306
2309
  if (term.type === ATOM) {
2307
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 = {}) {
@@ -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: () => {