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 +1 -1
- package/src/iso.js +11 -6
- package/src/parser.js +19 -3
- package/src/solver.js +18 -0
- package/src/write.js +12 -4
- package/test/run-regression.mjs +85 -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
|
}
|
|
@@ -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
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
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
|
|
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/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
|
|
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,
|
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: () => {
|
|
@@ -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: () => {
|