write(v:w:d) on a huge magnitude: three backends, three answers, none FPC's
- Type: bug (residual of [[bug-b-writeln-float-with-17-decimals-prints-garbage]]) — Track A
- Found: 2026-08-03, measured against FPC while fixing that ticket. The ordinary range is now FPC-identical on every backend; this is what is left.
Measured
WriteLn(1e300:0:5);
WriteLn((1.0/0.0*0.0):0:3); { NaN }
1e300:0:5 |
NaN | |
|---|---|---|
| FPC | 1.0E+0300 |
Nan |
| pxx x86-64 | 9223372036854775809.00000 |
-9223372036854775809.000 |
| pxx i386 / arm32 / riscv32 | the full 301-digit fixed expansion | (untested) |
The x86-64 answer is debris — cvttsd2si saturates to Int64's limit, and its
digits get printed. The runtime-helper backends produce the exact fixed
expansion, which is true but is neither FPC's nor x86-64's.
Three backends printing three different texts for one program is the part that matters most, more than which of them matches FPC.
Why it was left
The ordinary range — everything with |v| < 2^63 — is now FPC-identical on
both the codegen path and the runtime helper, which is what the parent ticket
was about. Fixing this corner needs one of:
- a runtime magnitude test in
EmitWriteFloatFixedbranching to the scientific writer, which doubles the emitted code at everywrite(v:w:d)call site; or - routing x86-64 (and aarch64) through
PXXWriteFloatFixedlike the other three backends — the right answer, and it also collapses THREE implementations of one formatter into one, but the helper takes nowidthargument, so its signature and every backend's call site have to change together.
The second is the real fix and is worth doing on its own terms.
Note on FPC as the oracle here
FPC's own answer is type-dependent and not worth matching digit for digit: a
literal 1e300 is Extended and prints a FOUR-digit exponent ( 1.0E+0300),
while the same value in a Double variable prints three ( 1.2E+300). Match
its SHAPE (fall back to an exponent form) rather than its exact spelling.
Gate
A Pascal test over 1e300, 1e60, DBL_MAX, NaN, +Inf, -Inf and
-1e300 at several :w:d, run on x86-64, i386, arm32 and riscv32, asserting
all four backends produce the SAME text — and that the text is an exponent form
rather than a fixed expansion, as FPC does.
Re-measured 2026-08-05 — the BUG half is fixed; what remains is the Track U fork
Two of the ticket's three complaints are gone, measured at HEAD:
| complaint | now |
|---|---|
x86-64 prints debris 9223372036854775809.00000 |
fixed — no saturation; the value is a real expansion |
| "three backends, three answers" | fixed — x86-64 / i386 / arm32 now print the IDENTICAL string |
| NaN / Inf print debris | fixed — Nan / Inf on every backend |
Fixed by today's float work: bug-a-x86-64-writeln-fixed-saturates-at-int64
(the Int64-scaling native emitter replaced by a shim onto PXXWriteFloatFixed),
bug-a-aarch64-float-field-width-ignored (the width parameter that made the
shim possible without losing padding), and
bug-a-writeln-nonfinite-float-aarch64-emitters-unchecked.
WriteLn(1e20:0:2) is now byte-identical to FPC on every target.
What is left is NOT a bug — it is the exact-vs-capped question
1e20:0:2 pxx 100000000000000000000.00 FPC 100000000000000000000.00 AGREE
1e30:0:3 pxx 1000000000000000140737488355328. FPC 1000000000000000000020000000000.00
1e300:0:5 pxx 99999999999999983567616651958... FPC 1.0E+0300
At 1e30 neither is the exact double — pxx prints the true value
(1000000000000000140737488355328), FPC prints its own approximation, because
FPC computes in Extended. At 1e300 FPC gives up on the fixed form entirely and
falls back to exponent notation, which is a third behaviour again.
So there is no single "FPC's answer" to match here, which is why this is now
blocked on decide-float-fixed-output-exact-or-fpc-17-digit-cap. That
decision has been updated with FPC's exponent-form fallback as evidence — it is
a third option nobody had written down.
Whichever way it goes, the implementation is one place now (PXXWriteFloatFixed)
rather than the three divergent backends this ticket was opened against.
CORRECTION 2026-08-06 — the claim "pxx prints the true value" was wrong
The note above states that at 1e30 "pxx prints the true value
(1000000000000000140737488355328), FPC prints its own approximation". The
second half is right; the first half is false. The exact value of the
double is 1000000000000000019884624838656. pxx's digits are wrong from about
the 17th significant digit, and at 1e300 wrong from the first.
The error in method is the one this repo has a rule against: I compared two
implementations against each other and concluded ours was the exact one,
without ever checking either against an oracle. decimal.Decimal(float(x))
answers it in one line.
What stands from that note: the x86-64 Int64 saturation is genuinely gone, the
backends genuinely agree now, NaN/Inf are genuinely correct, and
1e20:0:2 is genuinely byte-identical to FPC. What does not stand is the
characterisation of the large-magnitude output as exact.
Filed as [[bug-a-write-fixed-emits-false-digits-past-1e22]].
UPDATE 2026-08-06 — the digits are exact now; Str and WriteLn disagree
[[bug-a-write-fixed-emits-false-digits-past-1e22]] landed:
PXXWriteFloatFixed expands the integer part in base-10^9 integer limbs, so
WriteLn(1e30:0:2) prints 1000000000000000019884624838656.00 — the double's
exact value, byte-identical across x86-64 / i386 / arm32 / aarch64 / riscv32,
verified against decimal.Decimal over 3000 random doubles. So the "what are
the digits" question is settled and only the display policy is left, which
is [[decide-float-fixed-output-exact-or-fpc-17-digit-cap]].
That leaves a NEW, pxx-internal divergence for this ticket to carry, on top of
the FPC one it was opened for: Str(v:w:d) and WriteLn(v:w:d) no longer
agree. StrFloat (compiler/builtin/builtin.pas) hands anything at or past
9.2e18 to FloatToExpStr:
WriteLn(1e23:0:0) 99999999999999991611392
Str(1e23:0:0, s) 1e+23
Both are "a number rather than debris", which is what that branch was written
for, but they are two spellings of one operation answering differently. The
fix is the same routing — StrFloat's v >= 9.2e18 branch wants the exact
expansion as a string — and it is deliberately NOT done under the bug ticket,
because which form to print past the Int64 range is the parked decision this
ticket is blocked on. Do it when that resolves.