← board

C: (double) of a 64-bit integer truncates to 32 bits on i386 and arm32

Symptom

unsigned long long m = 0x1FFFFFFFFFFFFFULL;   /* 2^53-1, exact in a double */
printf("%.17g\n", (double)m);
x86-64 / aarch64 / riscv32 :  9007199254740991
i386 / arm32               :  -1

The rule, measured

The 64-bit value is truncated to its low 32 bits and then converted as a SIGNED 32-bit int. Every case fits:

value low 32 bits i386 result
2147483647 0x7FFFFFFF 2147483647 (correct by luck)
2147483648 0x80000000 -2147483648
4294967295 0xFFFFFFFF -1
4294967296 0x00000000 0
4886718345 0x23456789 591751049
9007199254740991 0xFFFFFFFF -1

Both signedness (long long and unsigned long long) and both float widths (double and float) are affected; (float)4294967296LL gives 0. Anything that fits in 31 bits is correct, which is why ordinary code does not notice.

It is the C frontend, not the backend

The identical conversion in Pascal is correct on the same targets:

var i: Int64; d: Double;
begin i := 9007199254740991; d := i; writeln(d:0:1); end.

gives 9007199254740991.0 on i386, arm32, riscv32, aarch64 and x86-64, matching FPC. So the 32-bit backends have a working int64→double path and the C cast is not using it.

Note riscv32 is correct as well — this is specific to i386 and arm32.

Why urgent

Silent, and the wrong value is not a near miss: -1 for nine quadrillion. Any C code converting a file size, a timestamp, a byte count or a hash to floating point is affected on two supported targets. It is invisible on x86-64, which is where everything is tested.

Repro

printf '#include <stdio.h>\nint main(void){ unsigned long long m = 0x1FFFFFFFFFFFFFULL;\n printf("%%.17g\\n", (double)m); return 0; }\n' > /tmp/u.c
./stable_linux_amd64/default/pinned --target=i386 /tmp/u.c /tmp/u_386 && qemu-i386 /tmp/u_386   # -1
./stable_linux_amd64/default/pinned /tmp/u.c /tmp/u_64 && /tmp/u_64                             # 9007199254740991

Separate, do not conflate

strtod("0x1p-1074") (the smallest subnormal) returns 0 on riscv32 only. That is soft-float flush-to-zero in the subnormal range, a different question from this one, and not chased — the user has asked that float rounding/representation work not absorb time. Recorded here so the next reader knows the two divergences in that test row are unrelated.

Log