eyeprolog 1.5.41 → 1.5.42
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/examples/book/README.md +1 -0
- package/examples/book/chapter-40/03-program-2.pl +2 -4
- package/examples/book/chapter-40/04-program-3.pl +4 -4
- package/examples/book/chapter-40/05-program-4.pl +5 -0
- package/package.json +1 -1
- package/src/parser.js +15 -4
- package/src/quads.js +73 -10
- package/test/run-regression.mjs +82 -0
- package/the-art-of-eyeprolog.md +20 -0
package/examples/book/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
% From The Art of EyeProlog, Chapter 40.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
?- read(X).
|
|
3
|
+
inputs("1."), X = 1, unexpected.
|
|
4
|
+
inputs("1."), peeks(" "), X = 1.
|
|
5
|
+
inputs("1. "), peeks(" "), X = 1, unexpected.
|
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -1051,10 +1051,21 @@ class Parser {
|
|
|
1051
1051
|
this.advance();
|
|
1052
1052
|
|
|
1053
1053
|
const answers = [];
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1054
|
+
// Issue #90 extends only the embedded answer-description grammar with
|
|
1055
|
+
// decimal-precision approximate equality. Do not leak `~` into ordinary
|
|
1056
|
+
// EyeProlog source syntax: temporarily parse it like the standard 700 xfx
|
|
1057
|
+
// comparison operators while consuming the indented quad answers.
|
|
1058
|
+
const previousApproximateOperator = this.infixOperators.get('~');
|
|
1059
|
+
this.infixOperators.set('~', { precedence: operatorStrength(700), associativity: 'none' });
|
|
1060
|
+
try {
|
|
1061
|
+
while (this.token.type !== TOK.EOF && this.sourceLineIsIndented(this.token.line)) {
|
|
1062
|
+
answers.push(this.parseTerm(0, true));
|
|
1063
|
+
this.expect(TOK.DOT, '.');
|
|
1064
|
+
this.advance();
|
|
1065
|
+
}
|
|
1066
|
+
} finally {
|
|
1067
|
+
if (previousApproximateOperator == null) this.infixOperators.delete('~');
|
|
1068
|
+
else this.infixOperators.set('~', previousApproximateOperator);
|
|
1058
1069
|
}
|
|
1059
1070
|
if (answers.length === 0) throw new Error(`parse line ${line}: quad requires an indented answer description`);
|
|
1060
1071
|
|
package/src/quads.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// A `maybe` annotation denotes a successful answer with residual constraints;
|
|
5
5
|
// it does not relax the ordinary answer-substitution comparison.
|
|
6
6
|
import {
|
|
7
|
-
ATOM, COMPOUND, VAR, Env, atom, compound, copyResolved, deref,
|
|
8
|
-
flattenConjunction, listFromItems, properListItems, termIsGround,
|
|
7
|
+
ATOM, COMPOUND, NUMBER, VAR, Env, atom, compound, copyResolved, deref,
|
|
8
|
+
flattenConjunction, isDecimalInteger, listFromItems, properListItems, termIsGround,
|
|
9
9
|
unify, variable,
|
|
10
10
|
} from './term.js';
|
|
11
11
|
import { parseGoalText } from './parser.js';
|
|
@@ -191,13 +191,14 @@ function malformedAlternative(query, alternative) {
|
|
|
191
191
|
for (const leaf of splitOperator(alternative, ';').map(describeLeaf)) {
|
|
192
192
|
if (leaf.malformed != null) return leaf.malformed;
|
|
193
193
|
const names = new Set();
|
|
194
|
-
|
|
194
|
+
const substitutions = [...leaf.bindings, ...leaf.approximations];
|
|
195
|
+
for (const binding of substitutions) {
|
|
195
196
|
const name = binding.args[0].name;
|
|
196
197
|
if (!queryNames.has(name) || names.has(name)) return binding;
|
|
197
198
|
names.add(name);
|
|
198
199
|
}
|
|
199
200
|
if (!leaf.sto) {
|
|
200
|
-
for (const binding of
|
|
201
|
+
for (const binding of substitutions) {
|
|
201
202
|
if (namedVariables(binding.args[1]).some((variable) => names.has(variable.name))) return binding;
|
|
202
203
|
}
|
|
203
204
|
}
|
|
@@ -208,6 +209,7 @@ function malformedAlternative(query, alternative) {
|
|
|
208
209
|
function describeLeaf(term) {
|
|
209
210
|
const leaf = {
|
|
210
211
|
bindings: [],
|
|
212
|
+
approximations: [],
|
|
211
213
|
unexpected: false,
|
|
212
214
|
more: false,
|
|
213
215
|
sto: false,
|
|
@@ -245,6 +247,11 @@ function describeLeaf(term) {
|
|
|
245
247
|
else leaf.bindings.push(item);
|
|
246
248
|
continue;
|
|
247
249
|
}
|
|
250
|
+
if (item.type === COMPOUND && item.name === '~' && item.arity === 2) {
|
|
251
|
+
if (item.args[0].type !== VAR || approximateDecimalInterval(item.args[1]) == null) leaf.malformed ??= item;
|
|
252
|
+
else leaf.approximations.push(item);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
248
255
|
if (item.type === COMPOUND && item.name === 'inputs' && item.arity === 1) {
|
|
249
256
|
const text = characterText(item.args[0]);
|
|
250
257
|
if (text == null || leaf.input != null) leaf.malformed ??= item;
|
|
@@ -265,7 +272,8 @@ function describeLeaf(term) {
|
|
|
265
272
|
if (isErrorDescription(item)) leaf.error = item;
|
|
266
273
|
else leaf.malformed ??= item;
|
|
267
274
|
}
|
|
268
|
-
leaf.hasExpectation = leaf.bindings.length > 0 || leaf.
|
|
275
|
+
leaf.hasExpectation = leaf.bindings.length > 0 || leaf.approximations.length > 0 ||
|
|
276
|
+
leaf.truth || leaf.false || leaf.maybe || leaf.loops || leaf.waits ||
|
|
269
277
|
leaf.error != null || leaf.output != null;
|
|
270
278
|
if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
|
|
271
279
|
if ([leaf.false, leaf.truth, leaf.error != null].filter(Boolean).length > 1) leaf.malformed ??= term;
|
|
@@ -404,7 +412,7 @@ function matchLeaf(program, query, leaf, actual, position) {
|
|
|
404
412
|
// its absence requires an unconstrained answer. In either case the stated
|
|
405
413
|
// substitutions remain exact and are checked below.
|
|
406
414
|
if (leaf.maybe !== hasPendingConstraints(solution.env)) return false;
|
|
407
|
-
return substitutionMatches(query, leaf.bindings, solution.env);
|
|
415
|
+
return substitutionMatches(query, leaf.bindings, leaf.approximations, solution.env);
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
function hasPendingConstraints(env) {
|
|
@@ -428,22 +436,73 @@ function alternativeDescribesLoop(alternative) {
|
|
|
428
436
|
return splitOperator(alternative, ';').some((term) => describeLeaf(term).loops);
|
|
429
437
|
}
|
|
430
438
|
|
|
431
|
-
function substitutionMatches(query, bindings, actualEnv) {
|
|
439
|
+
function substitutionMatches(query, bindings, approximations, actualEnv) {
|
|
432
440
|
const queryVariables = namedVariables(query);
|
|
433
441
|
const queryNames = new Set(queryVariables.map((variable) => variable.name));
|
|
442
|
+
const queryVariablesByName = new Map(queryVariables.map((variable) => [variable.name, variable]));
|
|
434
443
|
const expectedEnv = new Env();
|
|
435
444
|
const rebound = new Set();
|
|
436
|
-
for (const binding of bindings) {
|
|
445
|
+
for (const binding of [...bindings, ...approximations]) {
|
|
437
446
|
const variable = binding.args[0];
|
|
438
447
|
if (!queryNames.has(variable.name) || rebound.has(variable.name)) return false;
|
|
439
448
|
rebound.add(variable.name);
|
|
440
|
-
if (
|
|
449
|
+
if (binding.name === '=') {
|
|
450
|
+
if (!unify(variable, binding.args[1], expectedEnv)) return false;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const actualVariable = queryVariablesByName.get(variable.name);
|
|
454
|
+
const actualValue = deref(actualVariable, actualEnv);
|
|
455
|
+
if (!approximatelyMatches(actualValue, binding.args[1])) return false;
|
|
456
|
+
// Once the approximate predicate has accepted the actual float, bind the
|
|
457
|
+
// expected-side variable to that exact observed term. This lets the
|
|
458
|
+
// ordinary variant matcher continue to check the rest of the answer,
|
|
459
|
+
// including variable sharing, without turning `~` into a fuzzy unifier.
|
|
460
|
+
if (!unify(variable, copyResolved(actualValue, actualEnv), expectedEnv)) return false;
|
|
441
461
|
}
|
|
442
462
|
const expected = compound('$quad_answer', queryVariables.map((variable) => copyResolved(variable, expectedEnv)));
|
|
443
463
|
const actual = compound('$quad_answer', queryVariables.map((variable) => copyResolved(variable, actualEnv)));
|
|
444
464
|
return patternVariant(expected, new Env(), actual, new Env());
|
|
445
465
|
}
|
|
446
466
|
|
|
467
|
+
// Parse a quoted decimal spelling as the interval represented by rounding to
|
|
468
|
+
// its final written mantissa digit. For example, '14.2000' denotes
|
|
469
|
+
// [14.19995, 14.20005], and '1.42000e1' denotes the same interval. Construct
|
|
470
|
+
// the exact decimal endpoints before converting them to binary floats instead
|
|
471
|
+
// of adding a tiny tolerance to an already-rounded Number.
|
|
472
|
+
function approximateDecimalInterval(term) {
|
|
473
|
+
if (term?.type !== ATOM) return null;
|
|
474
|
+
const text = term.name;
|
|
475
|
+
const match = /^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/.exec(text);
|
|
476
|
+
if (match == null) return null;
|
|
477
|
+
|
|
478
|
+
const [, sign, integerDigits, fraction = '', exponentText = '0'] = match;
|
|
479
|
+
const digits = `${integerDigits}${fraction}`;
|
|
480
|
+
const unsignedMantissa = BigInt(digits || '0');
|
|
481
|
+
const mantissa = sign === '-' ? -unsignedMantissa : unsignedMantissa;
|
|
482
|
+
const exponent = Number(exponentText);
|
|
483
|
+
if (!Number.isSafeInteger(exponent)) return null;
|
|
484
|
+
const scale = exponent - fraction.length;
|
|
485
|
+
|
|
486
|
+
// center = mantissa * 10^scale; half an ulp of the written decimal is
|
|
487
|
+
// 5 * 10^(scale-1). Keeping the coefficient integral preserves every
|
|
488
|
+
// significant trailing zero in the expected spelling.
|
|
489
|
+
const lowerCoefficient = mantissa * 10n - 5n;
|
|
490
|
+
const upperCoefficient = mantissa * 10n + 5n;
|
|
491
|
+
const boundExponent = scale - 1;
|
|
492
|
+
const lower = Number(`${lowerCoefficient}e${boundExponent}`);
|
|
493
|
+
const upper = Number(`${upperCoefficient}e${boundExponent}`);
|
|
494
|
+
if (!Number.isFinite(lower) || !Number.isFinite(upper)) return null;
|
|
495
|
+
return { lower: Math.min(lower, upper), upper: Math.max(lower, upper) };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function approximatelyMatches(actual, expectedAtom) {
|
|
499
|
+
if (actual?.type !== NUMBER || isDecimalInteger(actual.name)) return false;
|
|
500
|
+
const actualValue = Number(actual.name);
|
|
501
|
+
if (!Number.isFinite(actualValue)) return false;
|
|
502
|
+
const interval = approximateDecimalInterval(expectedAtom);
|
|
503
|
+
return interval != null && actualValue >= interval.lower && actualValue <= interval.upper;
|
|
504
|
+
}
|
|
505
|
+
|
|
447
506
|
function namedVariables(term) {
|
|
448
507
|
const found = [];
|
|
449
508
|
const seen = new Set();
|
|
@@ -727,8 +786,12 @@ function formatFailure(program, quad, result, description = quad.answers[0]) {
|
|
|
727
786
|
}
|
|
728
787
|
|
|
729
788
|
function formatQuadTerm(program, term) {
|
|
789
|
+
const operators = [...program.operators.values()];
|
|
790
|
+
if (!operators.some(({ name, specifier }) => name === '~' && ['xfx', 'xfy', 'yfx'].includes(specifier))) {
|
|
791
|
+
operators.push({ priority: 700, specifier: 'xfx', name: '~' });
|
|
792
|
+
}
|
|
730
793
|
return formatTermForWrite(term, new Env(), {
|
|
731
794
|
quoted: true,
|
|
732
|
-
operators
|
|
795
|
+
operators,
|
|
733
796
|
});
|
|
734
797
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -994,6 +994,88 @@ why(
|
|
|
994
994
|
assertEqual(result.stdout, 'quads: 13 run, 13 passed, 0 failed.\n', 'quad report');
|
|
995
995
|
},
|
|
996
996
|
},
|
|
997
|
+
{
|
|
998
|
+
name: 'runQuads supports decimal-precision approximate float descriptions (issue #90)',
|
|
999
|
+
run: () => {
|
|
1000
|
+
const source = `?- V is 0+(3.2+11).
|
|
1001
|
+
` +
|
|
1002
|
+
` V ~ '14.2000'.
|
|
1003
|
+
|
|
1004
|
+
` +
|
|
1005
|
+
`?- V is 0+(3.2+11).
|
|
1006
|
+
` +
|
|
1007
|
+
` V ~ '1.42000e1'.
|
|
1008
|
+
|
|
1009
|
+
` +
|
|
1010
|
+
`?- V is -14.2.
|
|
1011
|
+
` +
|
|
1012
|
+
` V ~ '-14.2000'.
|
|
1013
|
+
|
|
1014
|
+
` +
|
|
1015
|
+
`?- V is 1.0e-3.
|
|
1016
|
+
` +
|
|
1017
|
+
` V ~ '1.000e-3'.
|
|
1018
|
+
|
|
1019
|
+
` +
|
|
1020
|
+
`?- V is 14.19995.
|
|
1021
|
+
` +
|
|
1022
|
+
` V ~ '14.2000'.
|
|
1023
|
+
|
|
1024
|
+
` +
|
|
1025
|
+
`?- V is 14.20005.
|
|
1026
|
+
` +
|
|
1027
|
+
` V ~ '14.2000'.
|
|
1028
|
+
|
|
1029
|
+
` +
|
|
1030
|
+
`?- V is 14.2.
|
|
1031
|
+
` +
|
|
1032
|
+
` V ~ '14.1999', unexpected.
|
|
1033
|
+
|
|
1034
|
+
` +
|
|
1035
|
+
`?- V is 14.
|
|
1036
|
+
` +
|
|
1037
|
+
` V ~ '14.0', unexpected.
|
|
1038
|
+
|
|
1039
|
+
` +
|
|
1040
|
+
`?- V is float(14).
|
|
1041
|
+
` +
|
|
1042
|
+
` V ~ '14.0'.
|
|
1043
|
+
`;
|
|
1044
|
+
const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'approx-quad.pl' }]));
|
|
1045
|
+
assertEqual(result.total, 9, 'approximate answer-description total');
|
|
1046
|
+
assertEqual(result.passed, 9, 'approximate answer-description passed');
|
|
1047
|
+
assertEqual(result.failed, 0, 'approximate answer-description failed');
|
|
1048
|
+
assertEqual(result.stdout, 'quads: 9 run, 9 passed, 0 failed.\n', 'approximate quad report');
|
|
1049
|
+
|
|
1050
|
+
const malformed = publicApi.runQuads(`?- V is 14.2.
|
|
1051
|
+
V ~ 14.2000.
|
|
1052
|
+
`);
|
|
1053
|
+
assertEqual(malformed.total, 1, 'numeric approximation total');
|
|
1054
|
+
assertEqual(malformed.failed, 1, 'numeric RHS is rejected');
|
|
1055
|
+
assertIncludes(malformed.stdout, 'MALFORMED', 'numeric RHS diagnostic');
|
|
1056
|
+
|
|
1057
|
+
const malformedAtom = publicApi.runQuads(`?- V is 14.2.
|
|
1058
|
+
V ~ 'fourteen'.
|
|
1059
|
+
`);
|
|
1060
|
+
assertEqual(malformedAtom.failed, 1, 'non-decimal atom is rejected');
|
|
1061
|
+
assertIncludes(malformedAtom.stdout, 'MALFORMED', 'non-decimal atom diagnostic');
|
|
1062
|
+
|
|
1063
|
+
const duplicate = publicApi.runQuads(`?- V is 14.2.
|
|
1064
|
+
V = 14.2, V ~ '14.2000'.
|
|
1065
|
+
`);
|
|
1066
|
+
assertEqual(duplicate.failed, 1, 'duplicate exact/approximate binding is rejected');
|
|
1067
|
+
assertIncludes(duplicate.stdout, 'MALFORMED', 'duplicate binding diagnostic');
|
|
1068
|
+
|
|
1069
|
+
let ordinaryApproximateParsed = false;
|
|
1070
|
+
try {
|
|
1071
|
+
Program.parse(`p(X,Y) :- X ~ Y.\n`);
|
|
1072
|
+
ordinaryApproximateParsed = true;
|
|
1073
|
+
} catch (_) {
|
|
1074
|
+
// `~` is answer-description syntax, not a normal-profile operator.
|
|
1075
|
+
}
|
|
1076
|
+
assertEqual(ordinaryApproximateParsed, false, 'quad approximate operator stays scoped to answer descriptions');
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
997
1079
|
{
|
|
998
1080
|
name: 'runQuads treats unexpected as a negative assertion on the next leaf (issue #83)',
|
|
999
1081
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -9597,6 +9597,26 @@ constraint remain. For example, a pending `dif/2` constraint can be checked as:
|
|
|
9597
9597
|
maybe, unexpected.
|
|
9598
9598
|
```
|
|
9599
9599
|
|
|
9600
|
+
ISO arithmetic examples sometimes describe a floating result only as
|
|
9601
|
+
"approximately equal" to a written decimal. Quad answer descriptions preserve
|
|
9602
|
+
the precision of that spelling with `~`: the right-hand side is a decimal atom,
|
|
9603
|
+
so trailing zeroes remain significant. `V ~ '14.2000'` accepts a **float** in
|
|
9604
|
+
the closed interval `14.19995` through `14.20005`; it does not accept an integer
|
|
9605
|
+
term, even when that integer has the same mathematical value. Exponent notation
|
|
9606
|
+
uses the last written mantissa digit in the same way, so `'1.42000e1'` denotes
|
|
9607
|
+
the same interval. For example:
|
|
9608
|
+
|
|
9609
|
+
```eyeprolog
|
|
9610
|
+
?- V is 0+(3.2+11).
|
|
9611
|
+
V ~ '14.2000'.
|
|
9612
|
+
```
|
|
9613
|
+
|
|
9614
|
+
This `~` notation belongs only to indented quad answer descriptions. It does
|
|
9615
|
+
not install an approximate-equality predicate or operator in ordinary Prolog
|
|
9616
|
+
source. A numeric right-hand side such as `V ~ 14.2000` is deliberately
|
|
9617
|
+
rejected because parsing it as a float would discard the written decimal
|
|
9618
|
+
precision that the check is intended to retain.
|
|
9619
|
+
|
|
9600
9620
|
EyeProlog treats native variable constraints, attributed-variable residue, and
|
|
9601
9621
|
delayed goals as pending residue for this purpose. Multiple
|
|
9602
9622
|
indented descriptions after one query are independent checks: each re-runs the
|