Measured 2026-09-10, compiler pascal26 at HEAD
Read this part first: it found ME, not the other way round
A 6000-pair ArcTan2 differential
([[bug-b-arctan-answers-nan-above-1e300-which-is-why-math-atan2-is-still-refused]])
came back with 2569 of 6000 ArcTan values disagreeing with glibc, against
a file header that says ArcTan is exact and a landed test that asserts it on
the bits. Every instinct said the RTL had a second defect. The disagreement was
the harness: the Pascal side had Val'd its decimal arguments into different
doubles than CPython had. Feeding the same sweep as raw hex bits, with a
round-trip precondition asserted and BRANCHED on, brought it to 0 of 6000.
A parser that silently returns a nearby double is an instrument that lies by being correct about something else — it does not error, it answers, and what it answers about is a number nobody asked for. Every float differential in this tree that reads its inputs as text is exposed to it, and the failure looks like a defect in whatever was being measured. That is a better argument for priority than the raw count below.
The contrast, which is the whole finding
6000 decimal strings, repr()-form doubles from a uniform spread of exponents
in [-320, 308], parsed three ways and compared as raw IEEE bits against
CPython's float():
| parser | wrong |
|---|---|
the COMPILER's float literal parser (x := 1.7e-199;) |
0 / 200 |
StrToFloat(s) — sysutils ParseFloatCore |
0 / 6000 |
Val(s, d, code) — builtin ValFloat |
1574 / 6000 |
Same strings, same program, same run. The compiler's own literal parse and sysutils' runtime parse are both correctly rounded; the third one is not.
Examples (string, what Val returns, the nearest double):
-1.701032016227767e-199 96AA0A86A0580940 96AA0A86A0580942
2.5713535225823995e-305 00B20E812BD140EA 00B20E812BD140E8
-9.018397582190982e+209 EB85F2020791FB9E EB85F2020791FB9C
6.513497922964598e+242 72586BB1B2AEB135 72586BB1B2AEB138
The mechanism
compiler/builtin/builtin.pas:2211, ValFloat:
mant := mant * 10.0 + (Ord(s[i]) - Ord('0')); { rounds, per digit }
...
mant := mant / scale; { rounds }
while expval > 0 do
if eneg then mant := mant / 10.0 else mant := mant * 10.0; { rounds, per decade }
Seventeen significant digits is seventeen roundings before the exponent is even
applied, and a 300-decade exponent is 300 more. Nothing here is a bug in any one
line; the algorithm simply is not a correctly-rounded decimal-to-binary
conversion. ParseFloatCore in lib/rtl/sysutils.pas is one.
Why the one-line fix is not available
compiler/builtin/builtin.pas is a BUILTIN unit. It cannot uses sysutils —
the same wall math.log and math.dist hit
([[bug-n-math-trunc-and-log-need-frontend-intercepts]]), and the reason
ValFloat was hand-rolled "pure float arithmetic — no libc" in the first place.
So the work is one of:
- move the correctly-rounded core into a place both can reach, and let
ParseFloatCoreandValFloatbecome two callers of one routine — the [[normalise-dont-special-case]] answer, and the one that stops this from happening a third time; or - port the correctly-rounded algorithm INTO builtin.pas, which leaves two implementations of one concept and is how this ticket exists.
Prefer 1. This is a textbook instance of the rule: of two paths for one concept, the one nobody extended is the one that stayed broken.
-dPXX_FLOAT_EXACT is not a workaround
It does not change this. That flag selects math.pas's double-double transcendental kernels; it has nothing to do with parsing, and the 1574 is identical with and without it. Anyone reaching for it as a workaround will measure no change.
A guard a fix must carry
A round-trip assertion over a spread that includes subnormals and the extremes,
compared on the BITS: Val(repr(d)) = d for every double in the sample. A
printed comparison cannot see this — the values differ in the last place or
two, and our own float formatter prints the shortest round-tripping decimal,
which will print the WRONG double's shortest form perfectly happily.