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 +1 -1
- package/src/iso.js +9 -6
- package/src/parser.js +19 -3
- package/test/run-regression.mjs +71 -0
package/package.json
CHANGED
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
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
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
|
|
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
|
-
|
|
1514
|
-
return numberTerm(source);
|
|
1530
|
+
return numberTerm(finiteFloatTokenText(source));
|
|
1515
1531
|
}
|
|
1516
1532
|
|
|
1517
1533
|
export function parseTermText(text, options = {}) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -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: () => {
|