← board

Int() of a large double is INT64_MIN on x86-64

Measured, pinned v339, x86-64

Int(3.5)      3.0                       correct
Int(1.0e18)   1000000000000000000.0     correct
Int(3.3e299)  -9.2233720368547758E+018  WRONG
Int(1.0e300)  -9.2233720368547758E+018  WRONG

FPC 3.2.2:

Int(1.0e300)   1.00000000000000000001E+0300
Int(3.3e299)   3.29999999999999999999E+0299
Frac(1.0e300)  0.00000000000000000000E+0000

-9.2233720368547758E+018 is INT64_MIN, the x86 "integer indefinite" result. So the builtin is still going through a 64-bit integer conversion. The 32-bit targets saturated at 2^31 and x86-64 lands on INT64_MIN — one defect, two word sizes, and the earlier fix addressed the symptom on two backends rather than the cause.

The fix needs no conversion at all

Int returns a DOUBLE. For |x| >= 2^52 every double is already an integer, so there is nothing to truncate:

if Abs(x) >= 4503599627370496.0 then Result := x      { 2^52 }
else <the existing Int64 route, which is safe below 2^52>

A magnitude test and an early return. lib/rtl/math.pas's DdRint already does exactly this and says why in a comment citing the older ticket.

Second facet, same builtin: Int LOSES THE SIGN OF ZERO

Found the same day, auditing SimpleRoundTo against FPC:

pxx FPC C's trunc()
Int(-0.5) +0.0 -0.0 -0.0
Int(-0.9) +0.0 -0.0 -0.0
Int(-0.0) +0.0 -0.0 -0.0
Int(-1.5) -1.0 -1.0 -1.0

Int truncates toward zero, so for x in (-1, 0] the true result is negative zero — IEEE 754 keeps the sign, and so do FPC and C. Only the magnitude-zero cases are affected, which is why it survived: Int(-1.5) is right.

It matters downstream. lib/rtl/math.pas's SimpleRoundTo is FPC's own formula (Int(AValue*RV - 0.5) / RV), and it diverges from FPC on 443 of 2,937 compared cases — every single one a +0.0 where FPC gives -0.0, and zero genuine value differences. Fix Int and all 443 go away. RoundTo matched FPC on all 2,937 because its formula routes through Round, not Int.

(Methodology note for whoever verifies: FPC's untyped real constants are Extended, so any input built from literal arithmetic hands the two compilers DIFFERENT doubles — 338 of 520 differed that way on a first attempt, which made RoundTo look broken when it is not. Build the inputs from Int64 bit patterns reinterpreted through a pointer.)

Also wrong: Frac

Frac(x) = x - Int(x), so it inherits this at the same magnitudes. FPC gives Frac(1e300) = 0. Check whether Frac is implemented in terms of Int or separately, and fix both — per devdocs/dev/normalise-dont-special-case.md, if the two have separate implementations the second one is the one that stays broken.

Not blocking the RTL

lib/rtl/math.pas's FMod used to be x - Int(x/y)*y and returned FMod(1e300, 3.0) = 1e300 because of this. Track B replaced it with an exact scaled-subtraction loop that never calls Int, so the RTL no longer depends on the builtin being right. This ticket stands for user code and for Frac.

Gate

make test + self-host byte-identical, plus Int and Frac matching FPC 3.2.2 at 1e300, 3.3e299, 2^52, 2^52-0.5, 2^63, and the negatives of each — on x86-64 and the 32-bit targets, so the earlier fix is not regressed.

Resolution (2026-08-15): already fixed at HEAD by 5b6e1728d — verified, both facets

fix(A): Int/Frac stay in the float domain on all five targets (5b6e1728d, landed after this ticket's pinned-v339 measurements) is exactly the fix this ticket asks for, and it went further than the write-up: EmitTruncToIntegralX64 does the pure bit manipulation (clear the fractional mantissa bits selected by the exponent), so inf/NaN pass through, |x| >= 2^52 is returned unchanged, and |x| < 1 becomes a SIGNED zero. No integer register appears in the lowering, so there is no range to run out of. i386 got EmitTruncToIntegral386 (x87 frndint with RC=truncate); riscv32/xtensa already routed through __pxx_dint.

Verified at HEAD (c304fca4d + this session's commits), self-hosted build:

No code change needed. Closing as fixed-elsewhere.

Log