← board

FPC numeric compatibility: Floor/Ceil return floats, Currency is a Double

1. Math.Floor / Math.Ceil return floats

lib/rtl/math.pas:54  function Floor(x: Double): Double;
lib/rtl/math.pas:55  function Ceil(x: Double): Double;
lib/rtl/math.pas:118 function Floor(x: Single): Single;
lib/rtl/math.pas:119 function Ceil(x: Single): Single;

FPC's Math.Floor and Math.Ceil return Integer, with Floor64 / Ceil64 returning Int64. Neither 64-bit variant exists here. What we have is C's floor()/ceil() semantics wearing FPC's names.

Two visible consequences:

The divergence has already propagated into our own tree, which is the strongest argument that it is wrong rather than a deliberate choice:

examples/raytracer/raytracer.pas:139   ix := Trunc(Floor(p.x));
examples/raytracer/raytracer_gui.pas:113   ix := Trunc(Floor(p.x));

That Trunc(...) wrapper exists only because Floor hands back a float. In FPC it is plain ix := Floor(p.x).

Migration cost is small but real: 8 call sites outside math.pas. Four are examples/mathf/mathdemo.pas, which asserts float results (Floor(-2.3) against -3.0) and would need its expectations changed; four are the two raytracers, which get simpler (drop the Trunc).

For the record, the rest of the family is correct and matches FPC — verified against the pinned compiler:

-2.7 -2.5 -1.5 -0.5 2.7
Round -2 -2 0
Trunc -2 2

2. Currency is a Double

lib/rtl/sysutils.pas:275  { FPC Currency is a fixed-point 4-decimal Int64; this RTL models it as
lib/rtl/sysutils.pas:278    Currency = Double;

The comment states the divergence outright. FPC's Currency is a scaled Int64 — exactly 4 decimal places, exact addition and subtraction. Ours is binary floating point, so the one type whose entire purpose is exact money cannot represent 0.10, and any monetary computation can drift from FPC's answer.

The substrate for a correct one already exists, so this is less work than it sounds: lib/rtl/bignum.pas (530 lines, signed add/sub/mul/compare, proven under RSA and P-256), plus the exact decimal↔digit-string core in sysutils (copied to compiler/exdec.inc). Nil Python already runs arbitrary-precision integer arithmetic on that base — 2 ** 100 and 10 ** 30 // 7 are exact. A fixed-point Currency is a scaled Int64 and does not even need the bignum path; it needs the scale-aware ops.

Acceptance

Floor/Ceil return Integer with Floor64/Ceil64 alongside, in-tree callers updated, and Currency is a fixed-point 4-decimal Int64 whose add/subtract are exact. Splitting this into two tickets is reasonable if someone takes only one half — they are filed together because they are one theme (FPC numeric surface) found in one pass, not because they must land together.

Log

Half 1 DONE 2026-08-09 (Track B): Floor/Ceil return Integer

Floor/Ceil return Integer and Floor64/Ceil64 return Int64, matching FPC — verified against an FPC build: Floor(-2.7) = -3, Ceil(-2.7) = -2, Floor(2.7) = 2, Ceil(2.7) = 3. Single overloads return Integer too, as FPC's do.

All 12 in-tree call sites updated, and the ticket's own evidence held up: the raytracers got SIMPLER. ix := Trunc(Floor(p.x)) is now ix := Floor(p.x), which is what the same line says in FPC. examples/mathf/mathdemo.pas gained a ChkI integer comparator for its four assertions.

Two hazards worth recording, because neither is obvious:

  1. An internal caller would have silently overflowed. DdRint — the ties-to-even helper under the double-double kernel — floors values up to 2^52. With Floor returning a 32-bit Integer, everything between 2^31 and 2^52 would have wrapped, in a function whose whole job is exact rounding. It now uses Int() directly, which is what it always meant (its operand is non-negative, so the two agree). This is exactly why FPC ships the 64-bit pair, and the new test pins Floor64 past 2^31 in both signs.

  2. floor/ceil are C names, and the return type was the only thing making a collision invisible. Before, Pascal Floor(Double): Double and C floor(double): double had identical signatures, so a hijack would have been undetectable; afterwards it would be loudly wrong. Measured before committing — with a deliberately broken Pascal Floor returning 0, C's floor still gave gcc's answer, so C resolves to crtl's. test/cmath_no_pascal_hijack.c keeps watching (bug-c-pascal-math-names-hijack-libc-through-pxxcio).

Half 2 (Currency = Double) — NOT done, and deliberately deferred

Left as filed, with blocked-by pointing at the COBOL costing ticket, because that is where the requirement actually lives. Reasons not to do it now:

This ticket's own text agrees splitting is reasonable; half 1 stands alone and is done.

2026-08-10 (Track B): the Floor/Ceil half is DONE; Currency stays parked

Half of this ticket has landed and the ticket did not say so. Verified by running it against $(PXX_STABLE):

Floor(2.7)=2            <- Integer, as FPC returns
Ceil(2.1)=3
Floor64(1e17)=100000000000000000
Ceil64(-1e17)=-100000000000000000

Math.Floor/Ceil return Integer and Floor64/Ceil64 exist. (That change is also what forced crtl to grow real floor/ceil bodies, since the C declarations had been binding to the Pascal pair — see regression-b113-floor-ceil-change-pulls-libm-into-system-libs-c.)

What remains is only the Currency half — FPC's Currency is a fixed-point 4-decimal Int64, ours is a Double, so a money type cannot represent 0.10 exactly. That is a representation change tied to the decimal/COBOL work this ticket is already blocked-by, and it sits in rainy-day.

Moved to rainy-day to match its blocker rather than idling in backlog where it reads as available. Related user call the same day (on bug-b-rounding-api-gaps-setroundmode-roundto-lround): we are not seeking 100% FPC float compliance. Currency is a different argument from tie-rounding — exact money is a real need, not bit-parity chasing — so this comes back if a money type is wanted, not because FPC does it.