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