eyeprolog 1.5.43 → 1.5.44
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/chapter-40/03-program-2.pl +1 -1
- package/package.json +1 -1
- package/src/parser.js +10 -15
- package/src/quads.js +123 -19
- package/test/run-regression.mjs +60 -22
- package/the-art-of-eyeprolog.md +28 -14
package/package.json
CHANGED
package/src/parser.js
CHANGED
|
@@ -176,6 +176,12 @@ export const PART3_OPERATOR_DEFINITIONS = [
|
|
|
176
176
|
// ISO 1200 fx definition.
|
|
177
177
|
export const QUAD_OPERATOR_DEFINITIONS = [
|
|
178
178
|
[1200, 'xfx', '?-'],
|
|
179
|
+
// Quad approximate-answer notation. Keep this visible through current_op/3
|
|
180
|
+
// at the same priority/specifier as the ISO comparison operators. There is
|
|
181
|
+
// deliberately no built-in predicate behind it: ordinary source may define
|
|
182
|
+
// or call ~~ / 2 in the usual way, while quads interpret it specially only
|
|
183
|
+
// inside answer descriptions.
|
|
184
|
+
[700, 'xfx', '~~'],
|
|
179
185
|
];
|
|
180
186
|
|
|
181
187
|
const CLPZ_OPERATOR_DEFINITIONS = [
|
|
@@ -1053,21 +1059,10 @@ class Parser {
|
|
|
1053
1059
|
this.advance();
|
|
1054
1060
|
|
|
1055
1061
|
const answers = [];
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const previousApproximateOperator = this.infixOperators.get('~');
|
|
1061
|
-
this.infixOperators.set('~', { precedence: operatorStrength(700), associativity: 'none' });
|
|
1062
|
-
try {
|
|
1063
|
-
while (this.token.type !== TOK.EOF && this.sourceLineIsIndented(this.token.line)) {
|
|
1064
|
-
answers.push(this.parseTerm(0, true));
|
|
1065
|
-
this.expect(TOK.DOT, '.');
|
|
1066
|
-
this.advance();
|
|
1067
|
-
}
|
|
1068
|
-
} finally {
|
|
1069
|
-
if (previousApproximateOperator == null) this.infixOperators.delete('~');
|
|
1070
|
-
else this.infixOperators.set('~', previousApproximateOperator);
|
|
1062
|
+
while (this.token.type !== TOK.EOF && this.sourceLineIsIndented(this.token.line)) {
|
|
1063
|
+
answers.push(this.parseTerm(0, true));
|
|
1064
|
+
this.expect(TOK.DOT, '.');
|
|
1065
|
+
this.advance();
|
|
1071
1066
|
}
|
|
1072
1067
|
if (answers.length === 0) throw new Error(`parse line ${line}: quad requires an indented answer description`);
|
|
1073
1068
|
|
package/src/quads.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// it does not relax the ordinary answer-substitution comparison.
|
|
6
6
|
import {
|
|
7
7
|
ATOM, COMPOUND, NUMBER, VAR, Env, atom, compound, copyResolved, deref,
|
|
8
|
-
flattenConjunction, isDecimalInteger, listFromItems, properListItems, termIsGround,
|
|
8
|
+
flattenConjunction, isDecimalInteger, listFromItems, numberTextFromDouble, properListItems, termIsGround,
|
|
9
9
|
unify, variable,
|
|
10
10
|
} from './term.js';
|
|
11
11
|
import { parseGoalText } from './parser.js';
|
|
@@ -247,7 +247,7 @@ function describeLeaf(term) {
|
|
|
247
247
|
else leaf.bindings.push(item);
|
|
248
248
|
continue;
|
|
249
249
|
}
|
|
250
|
-
if (item.type === COMPOUND && item.name === '
|
|
250
|
+
if (item.type === COMPOUND && item.name === '~~' && item.arity === 2) {
|
|
251
251
|
if (item.args[0].type !== VAR || approximateDecimalInterval(item.args[1]) == null) leaf.malformed ??= item;
|
|
252
252
|
else leaf.approximations.push(item);
|
|
253
253
|
continue;
|
|
@@ -456,7 +456,7 @@ function substitutionMatches(query, bindings, approximations, actualEnv) {
|
|
|
456
456
|
// Once the approximate predicate has accepted the actual float, bind the
|
|
457
457
|
// expected-side variable to that exact observed term. This lets the
|
|
458
458
|
// ordinary variant matcher continue to check the rest of the answer,
|
|
459
|
-
// including variable sharing, without turning
|
|
459
|
+
// including variable sharing, without turning `~~` into a fuzzy unifier.
|
|
460
460
|
if (!unify(variable, copyResolved(actualValue, actualEnv), expectedEnv)) return false;
|
|
461
461
|
}
|
|
462
462
|
const expected = compound('$quad_answer', queryVariables.map((variable) => copyResolved(variable, expectedEnv)));
|
|
@@ -464,11 +464,18 @@ function substitutionMatches(query, bindings, approximations, actualEnv) {
|
|
|
464
464
|
return patternVariant(expected, new Env(), actual, new Env());
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
//
|
|
468
|
-
// its final written mantissa digit.
|
|
469
|
-
// [14.19995, 14.20005], and '1.42000e1' denotes the same interval.
|
|
470
|
-
//
|
|
471
|
-
//
|
|
467
|
+
// A quad approximation atom denotes the closed decimal interval obtained by
|
|
468
|
+
// rounding to its final written mantissa digit. For example, '14.2000' denotes
|
|
469
|
+
// [14.19995, 14.20005], and '1.42000e1' denotes the same interval.
|
|
470
|
+
//
|
|
471
|
+
// The description is useful only if that exact decimal interval has meaningful
|
|
472
|
+
// resolution in EyeProlog's finite float set. Convert the lower endpoint,
|
|
473
|
+
// written midpoint, and upper endpoint to actual representable floats, using
|
|
474
|
+
// directed adjustment at the endpoints so the selected minimum/maximum remain
|
|
475
|
+
// inside the exact decimal interval. Require all three to be finite and
|
|
476
|
+
// strictly ascending. This rejects over-precise "fake float" descriptions whose
|
|
477
|
+
// decimal distinctions collapse to one implementation float, as well as
|
|
478
|
+
// overflow/continuation-value ranges.
|
|
472
479
|
function approximateDecimalInterval(term) {
|
|
473
480
|
if (term?.type !== ATOM) return null;
|
|
474
481
|
const text = term.name;
|
|
@@ -483,16 +490,112 @@ function approximateDecimalInterval(term) {
|
|
|
483
490
|
if (!Number.isSafeInteger(exponent)) return null;
|
|
484
491
|
const scale = exponent - fraction.length;
|
|
485
492
|
|
|
486
|
-
// center = mantissa * 10^scale; half
|
|
487
|
-
// 5 * 10^(scale-1).
|
|
488
|
-
//
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
493
|
+
// center = mantissa * 10^scale; half a unit in the final written decimal
|
|
494
|
+
// place is 5 * 10^(scale-1). Keep the decimal coefficients exact until the
|
|
495
|
+
// representable-float selection below.
|
|
496
|
+
const lowerDecimal = { coefficient: mantissa * 10n - 5n, exponent: scale - 1 };
|
|
497
|
+
const middleDecimal = { coefficient: mantissa, exponent: scale };
|
|
498
|
+
const upperDecimal = { coefficient: mantissa * 10n + 5n, exponent: scale - 1 };
|
|
499
|
+
|
|
500
|
+
const rawLower = decimalToFiniteFloat(lowerDecimal);
|
|
501
|
+
const middle = decimalToFiniteFloat(middleDecimal);
|
|
502
|
+
const rawUpper = decimalToFiniteFloat(upperDecimal);
|
|
503
|
+
if (rawLower == null || middle == null || rawUpper == null) return null;
|
|
504
|
+
// If nearest rounding already collapses any two of the three points, no
|
|
505
|
+
// directed endpoint adjustment can create three floats inside the interval.
|
|
506
|
+
// Reject here before exact decimal comparison, which also keeps absurdly
|
|
507
|
+
// over-precise exponents from constructing enormous BigInt powers.
|
|
508
|
+
if (!(rawLower.value < middle.value && middle.value < rawUpper.value)) return null;
|
|
509
|
+
|
|
510
|
+
// Number() rounds to nearest. If an endpoint rounded outside the closed
|
|
511
|
+
// decimal interval, move one representable float inward. This avoids
|
|
512
|
+
// accepting a float that is merely close to a decimal boundary but lies
|
|
513
|
+
// mathematically outside it.
|
|
514
|
+
let minimum = rawLower;
|
|
515
|
+
if (compareFiniteFloatToDecimal(minimum.value, lowerDecimal) < 0) {
|
|
516
|
+
minimum = canonicalFiniteFloat(nextUp(minimum.value));
|
|
517
|
+
}
|
|
518
|
+
let maximum = rawUpper;
|
|
519
|
+
if (compareFiniteFloatToDecimal(maximum.value, upperDecimal) > 0) {
|
|
520
|
+
maximum = canonicalFiniteFloat(nextDown(maximum.value));
|
|
521
|
+
}
|
|
522
|
+
if (minimum == null || maximum == null) return null;
|
|
523
|
+
|
|
524
|
+
if (!(minimum.value < middle.value && middle.value < maximum.value)) return null;
|
|
525
|
+
return { minimum, middle, maximum };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function decimalToFiniteFloat(decimal) {
|
|
529
|
+
const value = Number(`${decimal.coefficient}e${decimal.exponent}`);
|
|
530
|
+
return canonicalFiniteFloat(value);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function canonicalFiniteFloat(value) {
|
|
534
|
+
if (!Number.isFinite(value)) return null;
|
|
535
|
+
const text = numberTextFromDouble(value);
|
|
536
|
+
if (text == null || isDecimalInteger(text) || Number(text) !== value) return null;
|
|
537
|
+
return { value, text };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Compare an IEEE-754 binary64 value with coefficient * 10^exponent exactly.
|
|
541
|
+
// The float is converted to an integer over a power-of-two denominator, so no
|
|
542
|
+
// second floating rounding is involved in deciding whether a rounded boundary
|
|
543
|
+
// candidate lies inside or outside the decimal interval.
|
|
544
|
+
const FLOAT_BITS = new DataView(new ArrayBuffer(8));
|
|
545
|
+
function finiteFloatRatio(value) {
|
|
546
|
+
FLOAT_BITS.setFloat64(0, value, false);
|
|
547
|
+
const bits = FLOAT_BITS.getBigUint64(0, false);
|
|
548
|
+
const negative = (bits >> 63n) !== 0n;
|
|
549
|
+
const exponentBits = Number((bits >> 52n) & 0x7ffn);
|
|
550
|
+
const fraction = bits & 0xfffffffffffffn;
|
|
551
|
+
if (exponentBits === 0 && fraction === 0n) return { numerator: 0n, denominatorPower: 0 };
|
|
552
|
+
|
|
553
|
+
let significand;
|
|
554
|
+
let exponent2;
|
|
555
|
+
if (exponentBits === 0) {
|
|
556
|
+
significand = fraction;
|
|
557
|
+
exponent2 = -1074;
|
|
558
|
+
} else {
|
|
559
|
+
significand = (1n << 52n) | fraction;
|
|
560
|
+
exponent2 = exponentBits - 1023 - 52;
|
|
561
|
+
}
|
|
562
|
+
if (negative) significand = -significand;
|
|
563
|
+
if (exponent2 >= 0) {
|
|
564
|
+
return { numerator: significand << BigInt(exponent2), denominatorPower: 0 };
|
|
565
|
+
}
|
|
566
|
+
return { numerator: significand, denominatorPower: -exponent2 };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function compareFiniteFloatToDecimal(value, decimal) {
|
|
570
|
+
const { numerator, denominatorPower } = finiteFloatRatio(value);
|
|
571
|
+
let left;
|
|
572
|
+
let right;
|
|
573
|
+
if (decimal.exponent >= 0) {
|
|
574
|
+
left = numerator;
|
|
575
|
+
right = decimal.coefficient * (10n ** BigInt(decimal.exponent));
|
|
576
|
+
right <<= BigInt(denominatorPower);
|
|
577
|
+
} else {
|
|
578
|
+
const decimalDenominator = 10n ** BigInt(-decimal.exponent);
|
|
579
|
+
left = numerator * decimalDenominator;
|
|
580
|
+
right = decimal.coefficient << BigInt(denominatorPower);
|
|
581
|
+
}
|
|
582
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const NEXT_FLOAT_BITS = new DataView(new ArrayBuffer(8));
|
|
586
|
+
function nextUp(value) {
|
|
587
|
+
if (Number.isNaN(value) || value === Infinity) return value;
|
|
588
|
+
if (value === -Infinity) return -Number.MAX_VALUE;
|
|
589
|
+
if (value === 0) return Number.MIN_VALUE;
|
|
590
|
+
NEXT_FLOAT_BITS.setFloat64(0, value, false);
|
|
591
|
+
let bits = NEXT_FLOAT_BITS.getBigUint64(0, false);
|
|
592
|
+
bits += value > 0 ? 1n : -1n;
|
|
593
|
+
NEXT_FLOAT_BITS.setBigUint64(0, bits, false);
|
|
594
|
+
return NEXT_FLOAT_BITS.getFloat64(0, false);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function nextDown(value) {
|
|
598
|
+
return -nextUp(-value);
|
|
496
599
|
}
|
|
497
600
|
|
|
498
601
|
function approximatelyMatches(actual, expectedAtom) {
|
|
@@ -500,7 +603,8 @@ function approximatelyMatches(actual, expectedAtom) {
|
|
|
500
603
|
const actualValue = Number(actual.name);
|
|
501
604
|
if (!Number.isFinite(actualValue)) return false;
|
|
502
605
|
const interval = approximateDecimalInterval(expectedAtom);
|
|
503
|
-
return interval != null &&
|
|
606
|
+
return interval != null &&
|
|
607
|
+
actualValue >= interval.minimum.value && actualValue <= interval.maximum.value;
|
|
504
608
|
}
|
|
505
609
|
|
|
506
610
|
function namedVariables(term) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -995,51 +995,51 @@ why(
|
|
|
995
995
|
},
|
|
996
996
|
},
|
|
997
997
|
{
|
|
998
|
-
name: 'runQuads supports decimal-precision
|
|
998
|
+
name: 'runQuads supports realistic decimal-precision float descriptions with ~~ (issue #90)',
|
|
999
999
|
run: () => {
|
|
1000
1000
|
const source = `?- V is 0+(3.2+11).
|
|
1001
1001
|
` +
|
|
1002
|
-
` V
|
|
1002
|
+
` V ~~ '14.2000'.
|
|
1003
1003
|
|
|
1004
1004
|
` +
|
|
1005
1005
|
`?- V is 0+(3.2+11).
|
|
1006
1006
|
` +
|
|
1007
|
-
` V
|
|
1007
|
+
` V ~~ '1.42000e1'.
|
|
1008
1008
|
|
|
1009
1009
|
` +
|
|
1010
1010
|
`?- V is -14.2.
|
|
1011
1011
|
` +
|
|
1012
|
-
` V
|
|
1012
|
+
` V ~~ '-14.2000'.
|
|
1013
1013
|
|
|
1014
1014
|
` +
|
|
1015
1015
|
`?- V is 1.0e-3.
|
|
1016
1016
|
` +
|
|
1017
|
-
` V
|
|
1017
|
+
` V ~~ '1.000e-3'.
|
|
1018
1018
|
|
|
1019
1019
|
` +
|
|
1020
|
-
`?- V is 14.
|
|
1020
|
+
`?- V is 14.199950000000001.
|
|
1021
1021
|
` +
|
|
1022
|
-
` V
|
|
1022
|
+
` V ~~ '14.2000'.
|
|
1023
1023
|
|
|
1024
1024
|
` +
|
|
1025
1025
|
`?- V is 14.20005.
|
|
1026
1026
|
` +
|
|
1027
|
-
` V
|
|
1027
|
+
` V ~~ '14.2000'.
|
|
1028
1028
|
|
|
1029
1029
|
` +
|
|
1030
1030
|
`?- V is 14.2.
|
|
1031
1031
|
` +
|
|
1032
|
-
` V
|
|
1032
|
+
` V ~~ '14.1999', unexpected.
|
|
1033
1033
|
|
|
1034
1034
|
` +
|
|
1035
1035
|
`?- V is 14.
|
|
1036
1036
|
` +
|
|
1037
|
-
` V
|
|
1037
|
+
` V ~~ '14.0', unexpected.
|
|
1038
1038
|
|
|
1039
1039
|
` +
|
|
1040
1040
|
`?- V is float(14).
|
|
1041
1041
|
` +
|
|
1042
|
-
` V
|
|
1042
|
+
` V ~~ '14.0'.
|
|
1043
1043
|
`;
|
|
1044
1044
|
const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'approx-quad.pl' }]));
|
|
1045
1045
|
assertEqual(result.total, 9, 'approximate answer-description total');
|
|
@@ -1048,32 +1048,70 @@ why(
|
|
|
1048
1048
|
assertEqual(result.stdout, 'quads: 9 run, 9 passed, 0 failed.\n', 'approximate quad report');
|
|
1049
1049
|
|
|
1050
1050
|
const malformed = publicApi.runQuads(`?- V is 14.2.
|
|
1051
|
-
V
|
|
1051
|
+
V ~~ 14.2000.
|
|
1052
1052
|
`);
|
|
1053
1053
|
assertEqual(malformed.total, 1, 'numeric approximation total');
|
|
1054
1054
|
assertEqual(malformed.failed, 1, 'numeric RHS is rejected');
|
|
1055
1055
|
assertIncludes(malformed.stdout, 'MALFORMED', 'numeric RHS diagnostic');
|
|
1056
1056
|
|
|
1057
1057
|
const malformedAtom = publicApi.runQuads(`?- V is 14.2.
|
|
1058
|
-
V
|
|
1058
|
+
V ~~ 'fourteen'.
|
|
1059
1059
|
`);
|
|
1060
1060
|
assertEqual(malformedAtom.failed, 1, 'non-decimal atom is rejected');
|
|
1061
1061
|
assertIncludes(malformedAtom.stdout, 'MALFORMED', 'non-decimal atom diagnostic');
|
|
1062
1062
|
|
|
1063
1063
|
const duplicate = publicApi.runQuads(`?- V is 14.2.
|
|
1064
|
-
V = 14.2, V
|
|
1064
|
+
V = 14.2, V ~~ '14.2000'.
|
|
1065
1065
|
`);
|
|
1066
1066
|
assertEqual(duplicate.failed, 1, 'duplicate exact/approximate binding is rejected');
|
|
1067
1067
|
assertIncludes(duplicate.stdout, 'MALFORMED', 'duplicate binding diagnostic');
|
|
1068
1068
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1069
|
+
const fakeFloat = publicApi.runQuads(`?- V is 1.0.
|
|
1070
|
+
V ~~ '1.0000000000000001'.
|
|
1071
|
+
`);
|
|
1072
|
+
assertEqual(fakeFloat.failed, 1, 'fake-float midpoint spelling is malformed');
|
|
1073
|
+
assertIncludes(fakeFloat.stdout, 'MALFORMED', 'fake-float midpoint diagnostic');
|
|
1074
|
+
|
|
1075
|
+
const overPrecise = publicApi.runQuads(`?- V is 1.0000000000000002.
|
|
1076
|
+
V ~~ '1.0000000000000002'.
|
|
1077
|
+
`);
|
|
1078
|
+
assertEqual(overPrecise.failed, 1, 'interval without three distinct floats is malformed');
|
|
1079
|
+
assertIncludes(overPrecise.stdout, 'MALFORMED', 'over-precise interval diagnostic');
|
|
1080
|
+
|
|
1081
|
+
const continuationRange = publicApi.runQuads(`?- V is 1.0e308.
|
|
1082
|
+
V ~~ '1.0e309'.
|
|
1083
|
+
`);
|
|
1084
|
+
assertEqual(continuationRange.failed, 1, 'non-finite continuation-value interval is malformed');
|
|
1085
|
+
assertIncludes(continuationRange.stdout, 'MALFORMED', 'continuation-value diagnostic');
|
|
1086
|
+
|
|
1087
|
+
assertEqual(
|
|
1088
|
+
run('', { goal: 'current_op(Pri,Fix,=), current_op(Pri,Fix,~~)' }).stdout,
|
|
1089
|
+
'current_op(700, xfx, =), current_op(700, xfx, ~~).\n',
|
|
1090
|
+
'~~ shares the ISO equality operator priority and specifier',
|
|
1091
|
+
);
|
|
1092
|
+
assertEqual(
|
|
1093
|
+
run('', { goal: 'current_op(700,xfx,~~)' }).stats.completed_goal_lists,
|
|
1094
|
+
1,
|
|
1095
|
+
'~~ is visible through current_op/3 in the normal profile',
|
|
1096
|
+
);
|
|
1097
|
+
assertEqual(
|
|
1098
|
+
run('', { goal: 'current_op(_,_,~)' }).stats.completed_goal_lists,
|
|
1099
|
+
0,
|
|
1100
|
+
'~ is not installed by the quad approximation feature',
|
|
1101
|
+
);
|
|
1102
|
+
assertEqual(
|
|
1103
|
+
run('', { isoStrict: true, goal: "current_op(700,xfx,'~~')" }).stats.completed_goal_lists,
|
|
1104
|
+
0,
|
|
1105
|
+
'strict ISO Part 1 does not predefine the ~~ extension',
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
const ordinaryApproximate = Program.parse(`approx(X,Y) :- X ~~ Y.\n`);
|
|
1109
|
+
assertEqual(ordinaryApproximate.groups.has('user:~~/2'), false, '~~ has no built-in or implicit predicate definition');
|
|
1110
|
+
assertEqual(
|
|
1111
|
+
run(`~~(my,definition).\n`, { goal: 'my ~~ definition' }).stats.completed_goal_lists,
|
|
1112
|
+
1,
|
|
1113
|
+
'~~ remains available as an ordinary user-defined predicate',
|
|
1114
|
+
);
|
|
1077
1115
|
},
|
|
1078
1116
|
},
|
|
1079
1117
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -9608,23 +9608,37 @@ constraint remain. For example, a pending `dif/2` constraint can be checked as:
|
|
|
9608
9608
|
|
|
9609
9609
|
ISO arithmetic examples sometimes describe a floating result only as
|
|
9610
9610
|
"approximately equal" to a written decimal. Quad answer descriptions preserve
|
|
9611
|
-
the precision of that spelling with
|
|
9612
|
-
so trailing zeroes remain significant. `V
|
|
9613
|
-
the closed interval `14.19995` through `14.20005`; it does not accept an
|
|
9614
|
-
term, even when that integer has the same mathematical value. Exponent
|
|
9615
|
-
uses the last written mantissa digit in the same way, so `'1.42000e1'`
|
|
9616
|
-
the same interval. For example:
|
|
9611
|
+
the precision of that spelling with `~~`: the right-hand side is a decimal atom,
|
|
9612
|
+
so trailing zeroes remain significant. `V ~~ '14.2000'` accepts a **float** in
|
|
9613
|
+
the closed decimal interval `14.19995` through `14.20005`; it does not accept an
|
|
9614
|
+
integer term, even when that integer has the same mathematical value. Exponent
|
|
9615
|
+
notation uses the last written mantissa digit in the same way, so `'1.42000e1'`
|
|
9616
|
+
denotes the same interval. For example:
|
|
9617
9617
|
|
|
9618
9618
|
```eyeprolog
|
|
9619
9619
|
?- V is 0+(3.2+11).
|
|
9620
|
-
V
|
|
9621
|
-
```
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9620
|
+
V ~~ '14.2000'.
|
|
9621
|
+
```
|
|
9622
|
+
|
|
9623
|
+
`~~` is an EyeProlog normal-profile operator at priority 700 with specifier
|
|
9624
|
+
`xfx`, matching the priority/specifier of ISO comparison operators such as `=`;
|
|
9625
|
+
there is no built-in `~~/2` predicate. Quad answer descriptions interpret the
|
|
9626
|
+
operator specially as approximate float matching.
|
|
9627
|
+
|
|
9628
|
+
An approximation is accepted as well-formed only when its exact decimal
|
|
9629
|
+
interval contains at least three distinct, strictly ascending finite EyeProlog
|
|
9630
|
+
floats: the minimum representable float in the interval, the float denoted by
|
|
9631
|
+
the written midpoint, and the maximum representable float in the interval. This
|
|
9632
|
+
prevents a decimal spelling from claiming precision finer than the
|
|
9633
|
+
implementation can represent, including decimal spellings whose apparent
|
|
9634
|
+
precision collapses to the same implementation float, and rejects ranges that
|
|
9635
|
+
would require non-finite continuation values. Endpoint
|
|
9636
|
+
selection is directed inward, so a binary rounding of a decimal bound cannot
|
|
9637
|
+
admit a float that lies mathematically outside the closed decimal interval.
|
|
9638
|
+
|
|
9639
|
+
A numeric right-hand side such as `V ~~ 14.2000` is deliberately rejected
|
|
9640
|
+
because parsing it as a float would discard the written decimal precision that
|
|
9641
|
+
the check is intended to retain.
|
|
9628
9642
|
|
|
9629
9643
|
EyeProlog treats native variable constraints, attributed-variable residue, and
|
|
9630
9644
|
delayed goals as pending residue for this purpose. Multiple
|