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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.23",
6
+ "version": "1.3.25",
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 {
@@ -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
- stream.position = 0;
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
- return isDecimalInteger(term.name)
2303
- ? { integer: true, value: BigInt(term.name) }
2304
- : { integer: false, value: Number(term.name) };
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 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/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. While pullSolution() has readline suspended,
261
- // let ISO term input request a complete terminal term exactly when read/1-2
262
- // or read_term/2-3 actually executes. This also works inside conjunctions
263
- // and user predicates instead of only when read/* is the whole REPL goal.
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) {
@@ -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: () => {