From ce6533e696193eca10598272fcd96128cdfb4c19 Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Sat, 5 Sep 2026 06:06:46 +0200 Subject: [PATCH 1/2] Add tests for pow()/fpow() precision on a double-precision-clamped FPU On i386 the x87 FPU is the platform default and zend_init_fpu() clamps it to double precision (53-bit mantissa) for the whole request. libm's pow() computes on the x87 unit and relies on the extended range to round correctly to double, so while that clamp is in place exponentiation is off by one or more ULP. This matters because it makes exponentiation silently architecture- dependent: the same script prints different floats on i386 than on x86-64 or arm64, comparisons against precomputed constants fail, and the error propagates into anything built on it. Three code paths reach libm pow() and each gets its own file: Zend/tests/pow_fpu_precision.phpt the ** operator ext/standard/tests/math/pow_fpu_precision.phpt the pow() function ext/standard/tests/math/fpow_fpu_precision.phpt the fpow() function The operator and the function are covered by the same cases with the same inputs, so the two files differ only in call syntax and share an identical --EXPECT-- block. fpow() is kept separate because it bypasses safe_pow() and calls libm pow() directly, and because it additionally asserts the IEEE-754 special cases that must survive the FPU precision switch. Every expected value is exactly the correctly rounded double of its decimal literal, so a build that keeps full precision prints the short literal back and compares identical to it. Exact powers of two are included as reference points the clamp does not affect. These tests fail on i386 and pass wherever XPFPA_HAVE_CW is 0, which includes x86-64 and arm64. The following commit fixes i386. --- Zend/tests/pow_fpu_precision.phpt | 73 ++++++++++++++++++ .../tests/math/fpow_fpu_precision.phpt | 75 +++++++++++++++++++ .../tests/math/pow_fpu_precision.phpt | 73 ++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 Zend/tests/pow_fpu_precision.phpt create mode 100644 ext/standard/tests/math/fpow_fpu_precision.phpt create mode 100644 ext/standard/tests/math/pow_fpu_precision.phpt diff --git a/Zend/tests/pow_fpu_precision.phpt b/Zend/tests/pow_fpu_precision.phpt new file mode 100644 index 000000000000..74779b7ece9a --- /dev/null +++ b/Zend/tests/pow_fpu_precision.phpt @@ -0,0 +1,73 @@ +--TEST-- +The ** operator must not lose precision when the FPU is clamped to double precision +--INI-- +serialize_precision=-1 +--FILE-- + +--EXPECT-- +-- runtime -- +float(0.01) +float(0.001) +float(0.0001) +float(0.04) +float(0.008) +float(0.0016) +-- compile-time constant folding -- +float(0.01) +float(0.001) +float(0.0001) +float(0.04) +float(0.008) +float(0.0016) +-- exact powers of two are unaffected -- +float(0.5) +float(0.0009765625) +-- identical to the decimal literal -- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/math/fpow_fpu_precision.phpt b/ext/standard/tests/math/fpow_fpu_precision.phpt new file mode 100644 index 000000000000..f29272d8635e --- /dev/null +++ b/ext/standard/tests/math/fpow_fpu_precision.phpt @@ -0,0 +1,75 @@ +--TEST-- +fpow(): results must not lose precision when the FPU is clamped to double precision +--INI-- +serialize_precision=-1 +--FILE-- + +--EXPECT-- +-- precision -- +float(0.01) +float(0.001) +float(0.0001) +float(0.04) +float(0.008) +float(0.0016) +-- exact powers of two are unaffected -- +float(0.5) +float(0.0009765625) +-- identical to the decimal literal -- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +-- matches the ** operator -- +bool(true) +bool(true) +-- IEEE-754 semantics are preserved -- +float(INF) +float(-INF) +float(1) +float(1) +float(1) +float(NAN) diff --git a/ext/standard/tests/math/pow_fpu_precision.phpt b/ext/standard/tests/math/pow_fpu_precision.phpt new file mode 100644 index 000000000000..f4ad4a76450a --- /dev/null +++ b/ext/standard/tests/math/pow_fpu_precision.phpt @@ -0,0 +1,73 @@ +--TEST-- +pow() must not lose precision when the FPU is clamped to double precision +--INI-- +serialize_precision=-1 +--FILE-- + +--EXPECT-- +-- runtime -- +float(0.01) +float(0.001) +float(0.0001) +float(0.04) +float(0.008) +float(0.0016) +-- compile-time constant folding -- +float(0.01) +float(0.001) +float(0.0001) +float(0.04) +float(0.008) +float(0.0016) +-- exact powers of two are unaffected -- +float(0.5) +float(0.0009765625) +-- identical to the decimal literal -- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) From e9de18910701343bd58011ffbcdca2bc97b6e8e3 Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Fri, 4 Sep 2026 15:43:42 +0200 Subject: [PATCH 2/2] Use XPFPA for pow() and fpow() Restore extended FPU precision around the libm pow() call and truncate the result back to double, so that i386 returns the same correctly rounded doubles as every other platform. zend_pow() in zend_float.h wraps the call and is used from both safe_pow(), which backs the ** operator and pow(), and fpow(), which called libm pow() directly. It compiles down to a plain pow() wherever XPFPA_HAVE_CW is 0, which includes x86-64 and arm64. --- Zend/zend_float.h | 20 ++++++++++++++++++++ Zend/zend_operators.c | 2 +- ext/standard/math.c | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Zend/zend_float.h b/Zend/zend_float.h index 12aa02005c2a..33a130c0cc04 100644 --- a/Zend/zend_float.h +++ b/Zend/zend_float.h @@ -20,6 +20,8 @@ #include "zend_portability.h" +#include + BEGIN_EXTERN_C() /* @@ -413,4 +415,22 @@ END_EXTERN_C() #endif /* FPU CONTROL */ +/* zend_init_fpu() clamps the x87 FPU to double precision (53-bit mantissa) for + * the whole request. libm's pow() relies on the FPU running at extended + * precision internally, so while that clamp is in place its result is off by + * one or more ULP compared to platforms whose FPU has no precision control. + * Restore extended precision for the call and truncate the result back to + * double. Compiles down to a plain pow() everywhere XPFPA_HAVE_CW is 0, which + * includes x86-64 and arm64. */ +static zend_always_inline double zend_pow(double base, double exponent) +{ +#if XPFPA_HAVE_CW + XPFPA_DECLARE + XPFPA_SWITCH_DOUBLE_EXTENDED(); + XPFPA_RETURN_DOUBLE(pow(base, exponent)); +#else + return pow(base, exponent); +#endif +} + #endif diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index b1b1a39a536a..7873d6c6fa15 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -1347,7 +1347,7 @@ static double safe_pow(double base, double exponent) zend_power_base_0_exponent_lt_0_error(); } - return pow(base, exponent); + return zend_pow(base, exponent); } static zend_result ZEND_FASTCALL pow_function_base(zval *result, zval *op1, zval *op2) /* {{{ */ diff --git a/ext/standard/math.c b/ext/standard/math.c index e13f153f63e2..c2cb23ca4c5b 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -1485,7 +1485,7 @@ PHP_FUNCTION(fpow) Z_PARAM_DOUBLE(exponent) ZEND_PARSE_PARAMETERS_END(); - RETURN_DOUBLE(pow(base, exponent)); + RETURN_DOUBLE(zend_pow(base, exponent)); } /* }}} */