write(v:w:d): the fraction past ~16 digits is an approximation, padded with zeros
- Type: bug — SILENT wrong output, same class as
[[bug-a-write-fixed-emits-false-digits-past-1e22]], other half of the number.
Track A (
compiler/builtin/builtinheap.pas,PXXWriteFloatFixed). - Opened: 2026-08-06, measured while fixing the integer part. Not a regression — this half was never exact.
Measured (oracle: decimal.Decimal(float(x)), exact)
(1/3):0:30 pxx 0.333333333333333312000000000000
exact 0.333333333333333314829616256247
FPC 0.333333343300000000000000000000 (worse; not an oracle)
0.1:0:25 pxx 0.1000000000000000000000000
exact 0.1000000000000000055511151
Two shapes, both silent:
- digits 16-18 are the product's rounding, not the value's digits —
...312where the value has...314; - everything past 18 is zeros, presented as if the value ended there. For
0.1:0:25the zeros start at digit 17 because0.1 * 1e18rounds to exactly1e17, so the printed answer is a clean0.1followed by twenty-four zeros and not one digit of the real tail.
Cause
The fraction is scaled by a Double multiply and then divided down, and the routine's own comment records the padding as deliberate:
v := (x - ip) * pw + 0.5; { pw = 10^fdigits, fdigits capped at 18 }
...
i := fdigits + 1;
while i <= decimals do { past what a double knows: zeros, not guesses }
write('0');
"Past what a double knows" is the false premise. A double is
mant * 2^exp2 with both parts integral, so its decimal expansion is finite
and exact — a denormal runs to 767 digits and every one of them is a real
digit. The cap at 18 is a property of the Int64/Double scaling, not of the
value.
Fix
Same route the integer part just took: expand exactly in base-10^9 limbs. The
machinery is in the same unit (PxxSciMul / PxxSciSplit / PxxIntDDigits),
but the fraction needs the exp2 < 0 half of the expansion (multiply by 5^k)
plus digit extraction at an arbitrary offset with half-away-from-zero rounding
at the cut. lib/rtl/sysutils.pas's FmtFixed is the worked example — it does
exactly this for Format('%.Nf') and matches the exact value at every
magnitude and precision.
Note this is the fixed path only. PXXWriteFloatSci is already exact
(PxxSciDigits17).
Why it is low priority despite being silent
It only bites past ~16 fraction digits, where FPC is also wrong (differently), and no correct program can be relying on the current answer. Contrast the integer half, which broke at magnitudes a program reaches by accident.
Related
- [[bug-a-write-fixed-emits-false-digits-past-1e22]] — the integer half, fixed.
- [[decide-float-fixed-output-exact-or-fpc-17-digit-cap]] — the display-policy question. If that lands on "cap at 17 significant digits", this ticket becomes "print zeros ONLY past the cap, and the digits below it exactly", which is still a change: today digits 16-18 are wrong, not capped.
- [[compat-pascal-write-fixed-huge-magnitude-differs-from-fpc]] — the
Str(v:w:d)side, which bails to an exponent form past 9.2e18.
Gate
write(v:0:d) for d up to 30 matches decimal.Decimal(float(x)) quantized
half-away-from-zero, on every target; test/lib_writefloat_fixed.pas extended
with the cases above.
2026-08-07 — assessed, not started; and the DECISION is now unblocked
Two findings worth recording so the next attempt starts from them.
1. The blocking decision is ready for Track U.
[[decide-float-fixed-output-exact-or-fpc-17-digit-cap]] is blocked-by: bug-a-write-fixed-emits-false-digits-past-1e22 — and that ticket is done.
So the decision is unblocked and is the gating question for this one: it decides
whether the target is "every digit exact" or "exact below a 17-significant-digit
cap, zeros above". Both are a change from today (digits 16-18 are currently
wrong, not capped), but they are different implementations.
Worth noting for whoever answers it: pxx already prints the INTEGER half exactly, on all five targets, matching CPython against FPC's cap. So today the two halves of one number follow different rules — the integer part exact, the fraction approximated — which is an argument for "exact" on consistency grounds alone, independent of the FPC-parity question.
2. The fix is bigger than "port FmtFixed", because of the mirroring rule.
PXXWriteFloatFixed (builtinheap.pas) carries an explicit contract in its own
header: "Mirrors EmitWriteFloatFixed (x86-64), and must keep mirroring it: this
is the i386 / arm32 / riscv32 route to the same output, so a program's text must
not depend on which backend built it." So an exact base-10^9 expansion has to
land in the hand-emitted x86-64 float writer as well as the portable body,
or the targets diverge — and diverging text across backends is a worse bug than
the one being fixed.
The honest shape is therefore to route both to ONE shared Pascal helper, the way
the sci path already routes to PxxSciDigits17 (PXXWriteFloatSci is exact
precisely because it does this). That is the real task: not a numeric tweak but
removing a hand-written duplicate, which is also what stops this pair drifting
again.
Left in backlog at prio 35. It wants a session that can hold the emitter change and run the cross-target gate.