← board

write(v:w:d): the fraction past ~16 digits is an approximation, padded with zeros

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:

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.

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.