eyeprolog 1.3.23 → 1.3.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 +1 -1
- package/src/iso.js +21 -7
- package/src/parser.js +19 -3
- package/src/repl.js +18 -4
- package/test/run-regression.mjs +99 -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 {
|
|
@@ -1025,10 +1025,21 @@ function inputUnitBuiltin(name) {
|
|
|
1025
1025
|
throw new PrologError('permission_error(input, past_end_of_stream)', streamHandle(stream.id));
|
|
1026
1026
|
}
|
|
1027
1027
|
if (stream.pastEnd && stream.eofAction === 'reset') {
|
|
1028
|
-
|
|
1028
|
+
// A terminal EOF (Ctrl-D) is local to one input operation. The next
|
|
1029
|
+
// operation should wait for fresh terminal input rather than rewinding
|
|
1030
|
+
// and replaying the already-consumed interactive buffer.
|
|
1031
|
+
if (typeof stream.interactiveReadUnit !== 'function') stream.position = 0;
|
|
1029
1032
|
stream.pastEnd = false;
|
|
1030
1033
|
}
|
|
1031
1034
|
const peek = name.startsWith('peek');
|
|
1035
|
+
if (stream.position >= stream.content.length &&
|
|
1036
|
+
typeof stream.interactiveReadUnit === 'function') {
|
|
1037
|
+
const text = stream.interactiveReadUnit();
|
|
1038
|
+
if (text != null) {
|
|
1039
|
+
stream.content += String(text);
|
|
1040
|
+
stream.pastEnd = false;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1032
1043
|
let unit;
|
|
1033
1044
|
try {
|
|
1034
1045
|
unit = solver.io.readUnit(stream, peek);
|
|
@@ -1235,7 +1246,8 @@ function readTermFromStream(stream, solver) {
|
|
|
1235
1246
|
const term = parseReadTermText(candidate.text, solver);
|
|
1236
1247
|
stream.position = candidate.end;
|
|
1237
1248
|
return scopeReadTerm(term);
|
|
1238
|
-
} catch (
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
if (error instanceof NumberRepresentationError) throw new PrologError(error.formal);
|
|
1239
1251
|
// A dot inside a graphic operator, such as =.., is only a possible
|
|
1240
1252
|
// terminator. Keep scanning until a complete term parses.
|
|
1241
1253
|
}
|
|
@@ -1661,7 +1673,8 @@ function parseIsoNumber(text) {
|
|
|
1661
1673
|
const finite = Number(value.name);
|
|
1662
1674
|
if (!Number.isFinite(finite)) return null;
|
|
1663
1675
|
return numberTerm(numberTextFromDouble(finite));
|
|
1664
|
-
} catch (
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
if (error instanceof NumberRepresentationError) throw new PrologError(error.formal);
|
|
1665
1678
|
return null;
|
|
1666
1679
|
}
|
|
1667
1680
|
}
|
|
@@ -2299,9 +2312,10 @@ function evaluate(term, env) {
|
|
|
2299
2312
|
term = deref(term, env);
|
|
2300
2313
|
if (term.type === VAR) throw new PrologError('instantiation_error');
|
|
2301
2314
|
if (term.type === NUMBER) {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2315
|
+
if (isDecimalInteger(term.name)) return { integer: true, value: BigInt(term.name) };
|
|
2316
|
+
const value = Number(term.name);
|
|
2317
|
+
if (!Number.isFinite(value)) throw new PrologError('evaluation_error(float_overflow)');
|
|
2318
|
+
return { integer: false, value };
|
|
2305
2319
|
}
|
|
2306
2320
|
if (term.type === ATOM) {
|
|
2307
2321
|
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/src/repl.js
CHANGED
|
@@ -182,6 +182,18 @@ class LineReader {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
readInteractiveUnitSync() {
|
|
186
|
+
if (!this.canReadTermSynchronously()) return null;
|
|
187
|
+
this.output.write('|: ');
|
|
188
|
+
const line = this.readTerminalLineSync();
|
|
189
|
+
if (line == null) return null;
|
|
190
|
+
// The interactive line editor uses Enter to submit character input. Do
|
|
191
|
+
// not leave that submission newline buffered for the next get_/peek_
|
|
192
|
+
// call (matching SWI/Scryer top-level behaviour). An empty submitted
|
|
193
|
+
// line still represents an actual newline character.
|
|
194
|
+
return line.length === 0 ? '\n' : line;
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
readTerminalLineSync() {
|
|
186
198
|
const byte = Buffer.allocUnsafe(1);
|
|
187
199
|
const bytes = [];
|
|
@@ -257,11 +269,13 @@ function makeState(engine, sources, output, options = {}, previousState = null,
|
|
|
257
269
|
});
|
|
258
270
|
const userInput = solver.io.resolve('user_input');
|
|
259
271
|
if (userInput && reader?.canReadTermSynchronously()) {
|
|
260
|
-
// The solver is synchronous.
|
|
261
|
-
// let ISO
|
|
262
|
-
// or
|
|
263
|
-
// and user predicates instead of only when
|
|
272
|
+
// The solver is synchronous. While pullSolution() has readline suspended,
|
|
273
|
+
// let ISO input request terminal data exactly when read/1-2, read_term/2-3,
|
|
274
|
+
// or a character/code input predicate actually executes. This also works
|
|
275
|
+
// inside conjunctions and user predicates instead of only when an input
|
|
276
|
+
// predicate is the whole REPL goal.
|
|
264
277
|
userInput.interactiveReadTerm = () => reader.readInteractiveTermSync(solver);
|
|
278
|
+
userInput.interactiveReadUnit = () => reader.readInteractiveUnitSync();
|
|
265
279
|
}
|
|
266
280
|
const flagOverrides = new Map(previousState?.flagOverrides ?? []);
|
|
267
281
|
for (const [name, value] of flagOverrides) {
|
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: () => {
|
|
@@ -1890,6 +1961,34 @@ c4 ?- call((!;1)).
|
|
|
1890
1961
|
assertIncludes(result.stdout, ' true.', 'post-EOF query executes');
|
|
1891
1962
|
},
|
|
1892
1963
|
},
|
|
1964
|
+
{
|
|
1965
|
+
name: 'REPL character input is on demand and Ctrl-D stays local (issue #55)',
|
|
1966
|
+
run: () => {
|
|
1967
|
+
if (process.platform === 'win32') return;
|
|
1968
|
+
const available = spawnSync('sh', ['-c',
|
|
1969
|
+
'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
|
|
1970
|
+
if (available.status !== 0) return;
|
|
1971
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
1972
|
+
const scriptCommand =
|
|
1973
|
+
`{ printf 'peek_char(P), get_char(C), get_code(K).\n'; sleep 0.3; ` +
|
|
1974
|
+
`printf 'ab\n'; sleep 0.3; printf 'get_char(N).\n'; sleep 0.3; ` +
|
|
1975
|
+
`printf 'z\n'; sleep 0.3; printf 'get_char(E).\n'; sleep 0.3; ` +
|
|
1976
|
+
`printf '\\004'; sleep 0.3; printf 'get_char(D).\n'; sleep 0.3; ` +
|
|
1977
|
+
`printf 'q\n'; sleep 0.3; printf 'halt.\n'; } | ` +
|
|
1978
|
+
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
1979
|
+
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
1980
|
+
cwd: packageRoot,
|
|
1981
|
+
encoding: 'utf8',
|
|
1982
|
+
timeout: 5000,
|
|
1983
|
+
});
|
|
1984
|
+
assertEqual(result.error?.code, undefined, 'interactive character input timeout');
|
|
1985
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1986
|
+
assertIncludes(result.stdout, 'P = a, C = a, K = 98.', 'peek/get char and code input');
|
|
1987
|
+
assertIncludes(result.stdout, 'N = z.', 'Enter submits but is not buffered as the next character');
|
|
1988
|
+
assertIncludes(result.stdout, 'E = end_of_file.', 'Ctrl-D character result');
|
|
1989
|
+
assertIncludes(result.stdout, 'D = q.', 'character input resumes after Ctrl-D');
|
|
1990
|
+
},
|
|
1991
|
+
},
|
|
1893
1992
|
{
|
|
1894
1993
|
name: 'interactive user_input hook serves reads reached through user predicates',
|
|
1895
1994
|
run: () => {
|