From befc11a1fe8ae47492c988f309b988a2569b6055 Mon Sep 17 00:00:00 2001 From: Giandonn Date: Sat, 29 Aug 2026 20:59:31 -0300 Subject: [PATCH] fix(generator): float constants must not be truncated by the precision ini genCValue() lowered a float with a string cast, which formats using the precision ini. That defaults to 14, so a constant baked into the binary lost digits whenever the host PHP was not configured otherwise: // compiled by a host with the default precision=14 php::Var e = 2.718281828459; // M_E php::fn::log(2.718281828459); // 0.9999999999999832, PHP gives 1 The value is wrong in the binary itself, so nothing at runtime can recover it, and the same source compiled on two differently configured hosts produces two different programs. BinaryOpTrait::genFloatLiteral() already formats with %.17g and keeps the literal a C++ double; genCValue() simply was not using it. Both paths now render a float the same way. This is what makes three existing tests fail when the suite runs with the default precision, which run-tests.php sets itself: tests/compiler/basic/math_functions.phpt tests/compiler/class/readonly-class.phpt tests/compiler/type_decl/union-intersection-types.phpt All three pass with this change, verified against a real build. Two unit tests asserted the old spelling by comparing against `(string) $value`, which is the precision-dependent cast itself. They now assert what actually matters: the emitted literal reads back as the same double. FloatLiteralPrecisionTest covers the regression directly by compiling with precision=14 set. --- phpunit/code/float-constant-precision.php | 13 ++++++ phpunit/src/CompilerBaseApiTest.php | 6 ++- phpunit/src/FloatLiteralPrecisionTest.php | 50 +++++++++++++++++++++++ phpunit/src/Generator/UtilsTest.php | 7 +++- src/Generator/Utils.php | 4 +- 5 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 phpunit/code/float-constant-precision.php create mode 100644 phpunit/src/FloatLiteralPrecisionTest.php diff --git a/phpunit/code/float-constant-precision.php b/phpunit/code/float-constant-precision.php new file mode 100644 index 00000000..d5b8abf6 --- /dev/null +++ b/phpunit/code/float-constant-precision.php @@ -0,0 +1,13 @@ +assertSame((string) M_E, $this->invokeMethod('genCValue', M_E)); + // A string cast here would follow the precision ini, so the assertion + // is that the emitted literal reads back as the same double. + $this->assertSame(M_E, (float) $this->invokeMethod('genCValue', M_E)); $this->assertSame('1', $this->invokeMethod('genCValue', true)); $this->assertSame('0', $this->invokeMethod('genCValue', false)); @@ -370,7 +372,7 @@ public function testGeneratedCValuesAreAlwaysSourceCodeStrings(): void new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('M_E')) ); $this->assertIsString($code); - $this->assertSame((string) M_E, $code); + $this->assertSame(M_E, (float) $code); } public function testNumericStringIdentifiersGenerateSourceCodeStrings(): void diff --git a/phpunit/src/FloatLiteralPrecisionTest.php b/phpunit/src/FloatLiteralPrecisionTest.php new file mode 100644 index 00000000..df961a32 --- /dev/null +++ b/phpunit/src/FloatLiteralPrecisionTest.php @@ -0,0 +1,50 @@ +compileToCpp('float-constant-precision.php'); + } finally { + if ($previous !== false) { + ini_set('precision', $previous); + } + } + + self::assertStringContainsString('2.7182818284590451', $cpp); + self::assertStringNotContainsString('2.718281828459)', $cpp); + } + + private function compileToCpp(string $file): string + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + + return file_get_contents($compiler->convertFile($source)); + } +} diff --git a/phpunit/src/Generator/UtilsTest.php b/phpunit/src/Generator/UtilsTest.php index cc2264ea..56870e62 100644 --- a/phpunit/src/Generator/UtilsTest.php +++ b/phpunit/src/Generator/UtilsTest.php @@ -49,7 +49,12 @@ public function testGenCValueInt(): void public function testGenCValueFloat(): void { $result = $this->invokeMethod('genCValue', 3.14); - $this->assertSame((string) 3.14, $result); + + // The literal has to read back as the same double. Its exact spelling + // is not part of the contract, but a string cast would depend on the + // precision ini and lose digits. + $this->assertSame(3.14, (float) $result); + $this->assertMatchesRegularExpression('/[.E]/i', $result); } public function testGenCValueBool(): void diff --git a/src/Generator/Utils.php b/src/Generator/Utils.php index c243d81a..937c9809 100644 --- a/src/Generator/Utils.php +++ b/src/Generator/Utils.php @@ -31,7 +31,9 @@ protected function genCValue(mixed $value): string return $this->genIntegerLiteral($value); } if (is_float($value)) { - return (string) $value; + // A string cast formats with the precision ini, which defaults to + // 14 and would bake a truncated constant into the binary. + return $this->genFloatLiteral($value); } if (is_bool($value)) { return $value ? '1' : '0';