fix(parser): classify auto-Decimal literals by real mantissa precision (#50) --skip-tests
The >=16-significant-digit float-literal-to-php::Decimal promotion
(docs/en/HIGH_PRECISION_TYPES.md) counted every digit in the raw
literal with preg_replace('/[^0-9]/'), so exponent digits and trailing
zeros counted as significant: 1.23456789012345e300 (15 significant
digits) and 999999999999999.0 became Decimal, making
is_float(2.220446049250313E-16) compile to false. Hex literals whose
digits contain E (0x123456789E1234567) matched the [.eE] test and
became Decimal("0x..."), where Zend folds an overflowing hex literal
to its exact double.
Three fixes, keeping the documented feature:
- Count true mantissa significant digits (strip sign, exponent,
leading and trailing zeros) and additionally require that the double
cannot reproduce the literal exactly - a literal that round-trips
(every var_export/serialize output, PHP_FLOAT_EPSILON) has lost
nothing and stays float, while 3.14159265358979323846 still promotes.
- Exclude hex/octal/binary notation from the reclassification.
- When a Decimal-classified literal meets a float-typed expression in
a binary op or comparison, demote the literal to its exact double
instead of the "Cannot convert float expression to Decimal" fatal:
PHP evaluates every float literal as a double, so
0.1 + 0.2 == 0.30000000000000004 is valid PHP and must be true.
master
parent
d00c63a049
commit
407953c094
6 changed files with 241 additions and 3 deletions
@ -0,0 +1,31 @@ |
||||
<?php |
||||
|
||||
function fifteenSigDigitsWithExponent(): bool |
||||
{ |
||||
return is_float(1.23456789012345e300); |
||||
} |
||||
|
||||
function trailingZerosAreNotSignificant(): bool |
||||
{ |
||||
return is_float(999999999999999.0); |
||||
} |
||||
|
||||
function roundTripSixteenDigits(): bool |
||||
{ |
||||
return is_float(2.220446049250313E-16); |
||||
} |
||||
|
||||
function hexLiteralStaysNumeric(): float |
||||
{ |
||||
return 0x123456789E1234567; |
||||
} |
||||
|
||||
function autoDecimalKeepsPromotion() |
||||
{ |
||||
return 3.14159265358979323846; |
||||
} |
||||
|
||||
function decimalLiteralDemotesAgainstFloat(float $f): bool |
||||
{ |
||||
return $f == 3.14159265358979323846; |
||||
} |
||||
@ -0,0 +1,58 @@ |
||||
<?php |
||||
|
||||
use TypePhp\CompilerTest; |
||||
|
||||
/** |
||||
* The auto-Decimal promotion applies to decimal literals whose mantissa has |
||||
* 16+ significant digits AND whose value the double cannot reproduce |
||||
* exactly. Exponent digits, leading zeros and trailing mantissa zeros carry |
||||
* no precision; hex/octal/binary literals fold to their exact numeric value |
||||
* like Zend; and a Decimal-classified literal meeting a float-typed |
||||
* expression demotes to its exact double instead of failing to compile. |
||||
*/ |
||||
final class DecimalLiteralClassificationTest extends \BaseTest |
||||
{ |
||||
public function testOnlyGenuinePrecisionLossPromotesToDecimal(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
|
||||
// Exactly one literal (the 21-digit pi) is promoted... |
||||
self::assertSame(1, substr_count($code, 'php::toDecimal(')); |
||||
// ...and the borderline literals stay native floats, so every |
||||
// is_float() probe statically folds to true. |
||||
self::assertGreaterThanOrEqual(3, substr_count($code, 'php::toBool(true)')); |
||||
} |
||||
|
||||
public function testHexLiteralFoldsToExactDouble(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
|
||||
self::assertStringContainsString('2.0988295480315429e+19', $code); |
||||
self::assertStringNotContainsString('0x123456789E1234567', $code); |
||||
} |
||||
|
||||
public function testDecimalLiteralDemotesAgainstFloatTypedExpression(): void |
||||
{ |
||||
$code = $this->compileFixture(); |
||||
|
||||
// The comparison compiles (no "Cannot convert float expression to |
||||
// Decimal" fatal) and compares doubles like Zend. |
||||
self::assertStringContainsString('php::equals(f, 3.1415926535897931)', $code); |
||||
} |
||||
|
||||
private function compileFixture(): string |
||||
{ |
||||
global $translator; |
||||
|
||||
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator = $compiler; |
||||
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/decimal-literal-classification.php'; |
||||
$compiler->addFiles([$source]); |
||||
$compiler->prepareFile($source); |
||||
$generated = $compiler->convertFile($source); |
||||
$code = file_get_contents($generated); |
||||
|
||||
self::assertIsString($code); |
||||
return $code; |
||||
} |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
--TEST-- |
||||
Auto-Decimal literal classification: significant digits, hex, float mixing |
||||
--FILE-- |
||||
<?php |
||||
declare(strict_types=1); |
||||
|
||||
function main(): void |
||||
{ |
||||
// 15 significant digits (exponent digits carry no precision): float. |
||||
var_dump(is_float(1.23456789012345e300)); |
||||
// Trailing mantissa zeros carry no precision: float. |
||||
var_dump(is_float(999999999999999.0)); |
||||
// 16 digits, but the double reproduces the value exactly: float. |
||||
var_dump(is_float(2.220446049250313E-16)); |
||||
// Hex folds to its exact numeric value like Zend. |
||||
var_dump(0x123456789E1234567); |
||||
// var_export round-trip comparisons stay plain float comparisons. |
||||
var_dump(0.1 + 0.2 == 0.30000000000000004); |
||||
$f = 0.1; |
||||
var_dump($f + 0.2 == 0.30000000000000004); |
||||
// 21 significant digits still promote to Decimal (documented feature). |
||||
var_dump(is_float(3.14159265358979323846)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
bool(true) |
||||
bool(true) |
||||
float(2.098829548031543E+19) |
||||
bool(true) |
||||
bool(true) |
||||
bool(false) |
||||
Loading…
Reference in new issue