← board

x86-64: d := q for a QWord above 2^63 produces half the value

Symptom

var q: QWord; d: Double;
begin q := 18446744073709551615; d := q; writeln(d:0:1); end.
FPC 18446744073709552000.0
pxx x86-64 9223372036854775809.0
pxx i386 / arm32 / riscv32 18446744073709551616.0 (correct value — see note)
pxx aarch64 see bug-a-aarch64-large-double-decimal-formatting

The 32-bit targets' ...551616.0 and FPC's ...552000.0 are the SAME double printed to different precision — a formatting difference, deliberately not chased. x86-64's answer is a different NUMBER (≈2^63), and that is the bug.

Why x86-64 specifically

cvtsi2sd is signed and x86-64 has no unsigned-64→double instruction, so the unsigned case needs the halve/convert/double sequence. The Int() intrinsic path (specialId = 206 in ir_codegen.inc) got exactly that sequence in bug-c-int64-to-double-cast-truncates-on-32bit and is now correct. The assignment conversion d := q is a different site and still emits a bare signed convert. Same fix, different place — see the 206 branch for the sequence to copy.

Repro

printf 'var q: QWord; d: Double;\nbegin q := 18446744073709551615; d := q; writeln(d:0:1); end.\n' > /tmp/q.pas
./compiler/pascal26 /tmp/q.pas /tmp/q_p && /tmp/q_p     # 9223372036854775809.0

Note for whoever takes it

This is the third site in the same family (Int() intrinsic, C (double) cast, Pascal assignment), which is the argument for the structural ticket feature-a-unify-32bit-call-argument-marshalling makes about call marshalling: the int→float conversion ladder is likewise written out once per site instead of once. Consider a shared EmitIntToFloat(dstReg, srcTk) per backend — i386 and arm32 already have one (EmitIntToXmm386, EmitIntToD0Arm32Tk) and the bug in both cases was a site that did not call it.

Resolution (2026-08-05) — RE-TRIAGED: the conversion is correct; the WRITER is not

The ticket blames the assignment conversion d := q. Measured, that conversion is right, and the ticket's own repro was reading the result through a broken formatter.

q := 18446744073709551615;
d := q;
writeln('sci   : ', d);       { pxx  1.8446744073709552E+019   FPC  1.8446744073709552E+019 }
writeln('fixed : ', d:0:1);   { pxx  9223372036854775809.0     FPC  18446744073709552000.0  }
e := 18446744073709551616.0;
writeln(Ord(d = e));          { 1 — the double is exactly the literal }

The scientific spelling of the SAME double matches FPC exactly, and d = e against the literal 18446744073709551616.0 is TRUE. So the QWord→Double conversion produces the correct bits; EmitCvtSi2SdOrU64 already carries the round-to-odd halve/convert/double fixup and the store path at ir_codegen.inc calls it with the value node's type (tyUInt64, confirmed in the IR dump). Nothing to fix there.

9223372036854775809 is 2^63+1 — the Int64 saturation value, and it comes from the fixed-decimals WRITER, not the conversion. Same number the aarch64 comparison produced for writeln(1e20:0:2) earlier today.

This is a duplicate of bug-a-x86-64-writeln-fixed-saturates-at-int64, filed earlier today from the other direction. Closing as such; that ticket has been enriched with this second repro.

Why the original diagnosis was reasonable and still wrong

The ticket reasoned from "the Int() intrinsic path got the halve/convert/double sequence and is now correct, so the assignment site must be the one still missing it". That inference was sound — there really are three sites in that family — but the assignment site had already been fixed (or never lacked it), and the surviving ≈2^63 in the output came from a different layer entirely. The lesson is the one in the debugging playbook: the printed value went through TWO conversions, and only one of them was under suspicion. Comparing the same double through a second spelling (writeln(d) vs writeln(d:0:1)) separated them in one run.

Its structural point still stands

The closing note argues the int→float ladder should be one EmitIntToFloat(dstReg, srcTk) per backend rather than once per site. x86-64 already has exactly that (EmitCvtSi2SdOrU64) and it is why this site was fine. The observation is right; it is simply already done on this target.

Log