diff --git a/phpunit/code/float-declaration-metadata.php b/phpunit/code/float-declaration-metadata.php new file mode 100644 index 00000000..df6bae19 --- /dev/null +++ b/phpunit/code/float-declaration-metadata.php @@ -0,0 +1,19 @@ +assertSame((string) M_E, $this->invokeMethod('genCValue', M_E)); + $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 +370,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/Generator/UtilsTest.php b/phpunit/src/Generator/UtilsTest.php index cc2264ea..c7dc7a46 100644 --- a/phpunit/src/Generator/UtilsTest.php +++ b/phpunit/src/Generator/UtilsTest.php @@ -49,7 +49,8 @@ public function testGenCValueInt(): void public function testGenCValueFloat(): void { $result = $this->invokeMethod('genCValue', 3.14); - $this->assertSame((string) 3.14, $result); + $this->assertSame(3.14, (float) $result); + $this->assertMatchesRegularExpression('/[.E]/i', $result); } public function testGenCValueBool(): void diff --git a/phpunit/src/OperatorTest.php b/phpunit/src/OperatorTest.php index 3afa9d9f..09c2bdff 100644 --- a/phpunit/src/OperatorTest.php +++ b/phpunit/src/OperatorTest.php @@ -85,4 +85,81 @@ public function testLiteralModuloAssignByZeroDoesNotCompile(): void { $this->exec('Cannot divide or modulo by zero', 'assign-modulo-by-zero.php'); } + + public function testFloatLiteralSpecialValuesAndWholeNumbers(): void + { + $previous = ini_set('precision', '14'); + try { + global $translator; + $compiler = \TypePhp\CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $testFile = __DIR__ . '/../code/float-literal-special.php'; + $compiler->addFiles([$testFile]); + $compiler->prepareFile($testFile); + $cppFile = $compiler->convertFile($testFile); + $cpp = file_get_contents($cppFile); + } finally { + if ($previous !== false) { + ini_set('precision', $previous); + } + } + + $this->assertStringContainsString('1.0', $cpp); + $this->assertStringContainsString('0.0', $cpp); + $this->assertStringContainsString('std::numeric_limits::infinity()', $cpp); + $this->assertStringContainsString('-std::numeric_limits::infinity()', $cpp); + $this->assertStringContainsString('std::numeric_limits::quiet_NaN()', $cpp); + $this->assertStringContainsString('2.7182818284590451', $cpp); + $this->assertStringNotContainsString('2.718281828459)', $cpp); + } + + public function testFloatDeclarationMetadataIgnoresHostPrecisionAndHandlesSpecialValues(): void + { + $previous = ini_set('precision', '14'); + try { + global $translator; + $compiler = \TypePhp\CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $testFile = TYPEPHP_ROOT_PATH . '/phpunit/code/float-declaration-metadata.php'; + $compiler->addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + $arginfoHeader = $compiler->getArgInfoHeaderFile($testFile); + $arginfo = file_get_contents($arginfoHeader); + $extension = file_get_contents($compiler->genExtension()); + } finally { + if ($previous !== false) { + ini_set('precision', $previous); + } + } + + $this->assertStringContainsString('ZVAL_DOUBLE(&const_POSITIVE_INF_value, std::numeric_limits::infinity());', $arginfo); + $this->assertStringContainsString('ZVAL_DOUBLE(&const_NEGATIVE_INF_value, -std::numeric_limits::infinity());', $arginfo); + $this->assertStringContainsString('ZVAL_DOUBLE(&const_NOT_A_NUMBER_value, std::numeric_limits::quiet_NaN());', $arginfo); + $this->assertStringContainsString('ZVAL_DOUBLE(&const_CONST_E_value, 2.7182818284590451);', $arginfo); + $this->assertStringContainsString('ZVAL_DOUBLE(&const_CONST_ONE_POINT_FIVE_value, 1.5);', $arginfo); + $this->assertStringNotContainsString('2.718281828459);', $arginfo); + + $this->assertStringContainsString('php::toFloat(2.7182818284590451)', $extension); + $this->assertStringContainsString('php::toFloat(std::numeric_limits::infinity())', $extension); + $this->assertStringContainsString('php::toFloat(std::numeric_limits::quiet_NaN())', $extension); + $this->assertStringNotContainsString('2.718281828459)', $extension); + } + + public function testFloatLiteralEmissionIsLocaleIndependent(): void + { + $previousLocale = setlocale(LC_ALL, 'de_DE.UTF-8', 'da_DK.UTF-8', 'en_DK.utf8'); + try { + global $translator; + $compiler = \TypePhp\CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $result = $compiler->genFloatLiteral(1.5); + $this->assertSame('1.5', $result); + $this->assertStringNotContainsString(',', $result); + } finally { + if ($previousLocale !== false) { + setlocale(LC_ALL, $previousLocale); + } + } + } } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index c5327c65..29e11025 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -395,7 +395,6 @@ protected function getBoolValue(Expr\ConstFetch $expr): string protected array $linkPaths = []; // --link-path / -L: user-specified library search paths /** @var list Required PHP modules recorded in zend_module_entry.deps. */ protected array $extensionDependencies = []; - protected int $floatPrecision = 17; protected bool $debug = false; protected bool $formatCode = false; // --format: enable clang-format (disabled by default) protected bool $printBacktraceOnError = true; @@ -4246,18 +4245,7 @@ protected function parseInclude(Expr\Include_ $expr): string protected function parseScalarFloat(Node\Scalar\Float_ $expr): string { - $value = $expr->value; - - if (is_nan($value)) { - return self::VALUE_NAN; - } - if (is_infinite($value)) { - return $value > 0 ? self::VALUE_INF : '-' . self::VALUE_INF; - } - if (floor($value) == $value && abs($value) < 1e15) { - return number_format($value, 1, '.', ''); - } - return sprintf('%.' . $this->floatPrecision . 'g', $value); + return $this->genFloatLiteral($expr->value); } protected function parseIsset(Expr\Isset_ $expr): string diff --git a/src/Generator/Utils.php b/src/Generator/Utils.php index c243d81a..a0c17fd3 100644 --- a/src/Generator/Utils.php +++ b/src/Generator/Utils.php @@ -25,13 +25,29 @@ protected function genIntegerLiteral(int $value): string return $value . $this->getPlatform()->getIntegerLiteralSuffix(); } + public function genFloatLiteral(float $value): string + { + if (is_nan($value)) { + return self::VALUE_NAN; + } + if (is_infinite($value)) { + return $value > 0 ? self::VALUE_INF : '-' . self::VALUE_INF; + } + $text = sprintf('%.17h', $value); + // Make sure the literal is parsed as a C++ double. + if (!str_contains($text, '.') && !str_contains(strtolower($text), 'e')) { + $text .= '.0'; + } + return $text; + } + protected function genCValue(mixed $value): string { if (is_int($value)) { return $this->genIntegerLiteral($value); } if (is_float($value)) { - return (string) $value; + return $this->genFloatLiteral($value); } if (is_bool($value)) { return $value ? '1' : '0'; diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index a97f653c..8ba4e9aa 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -490,15 +490,6 @@ protected function handleNestedConstantDivisionByZero( return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))'; } - protected function genFloatLiteral(float $value): string - { - $text = sprintf('%.17g', $value); - // Make sure the literal is parsed as a C++ double. - if (!str_contains($text, '.') && !str_contains(strtolower($text), 'e')) { - $text .= '.0'; - } - return $text; - } protected function shouldMaterializeOrderedOperand(NodeAbstract $expr): bool { diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index 28b0b6a5..42542098 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -236,13 +236,7 @@ protected function genInternalScalarConstantValue(mixed $value): string return $this->genIntegerLiteral($value); } if (is_float($value)) { - if (is_nan($value)) { - return self::VALUE_NAN; - } - if (is_infinite($value)) { - return $value > 0 ? self::VALUE_INF : '-' . self::VALUE_INF; - } - return $this->genCValue($value); + return $this->genFloatLiteral($value); } if (is_bool($value)) { return $value ? 'true' : 'false'; diff --git a/src/gen_stub.php b/src/gen_stub.php index 85547588..f140dad8 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -2827,8 +2827,10 @@ public function getCExpr(): ?string // reduced constant string expressions. Emitting that value avoids // leaking heredoc/nowdoc source syntax into generated C++. return '"' . getTranslator()->escapeString((string) $this->value) . '"'; - } elseif ($this->type->isInt() or $this->type->isFloat()) { + } elseif ($this->type->isInt()) { return strval($this->value); + } elseif ($this->type->isFloat()) { + return getTranslator()->genFloatLiteral((float) $this->value); } elseif ($this->type->isBool()) { return $this->value ? 'true' : 'false'; }