Skip to content

Commit a37e168

Browse files
author
Giandonn
committed
fix(optimizer): round() must not lower a RoundingMode enum to an int
Since PHP 8.4 the third parameter of round() is a RoundingMode enum, but php::fn::round() models the mode as an Int, which only covers the legacy PHP_ROUND_* constants. genRound sent the argument through convertIntExpr regardless, so the enum went through an object-to-int conversion that yields 1 - PHP_ROUND_HALF_UP: round(2.5, 0, RoundingMode::HalfEven); // compiled: Warning: Object of class RoundingMode could not be // converted to int // compiled: 3 // PHP: 2 Banker's rounding silently became half away from zero. Code that spells out HalfEven is usually money code, where that is the exact difference it was avoiding. A mode that is not statically an int now falls through to the dynamic path, which passes the enum to the runtime function unchanged. The legacy integer constants keep the native call - PHP_ROUND_HALF_DOWN still lowers to a plain 2L - and the one and two argument forms are untouched. Only the compile-time lowering is covered by a test here. A runtime PHPT cannot pass yet: phpx resolves a class constant on an internal class by reading the raw zval out of the constants table, so RoundingMode::HalfEven does not materialise at all. That is reported separately; once it ships, the runtime case can be added to type_conv-style coverage.
1 parent b493ac7 commit a37e168

5 files changed

Lines changed: 83 additions & 1 deletion

File tree

‎hello‎

79.3 KB
Binary file not shown.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
/**
3+
* This file is part of TypePHP(AOT).
4+
*
5+
* @link https://www.swoole.com/aot/
6+
* @contact service@swoole.com
7+
*/
8+
9+
function main(): void
10+
{
11+
var_dump(round(2.5, 0, PHP_ROUND_HALF_DOWN));
12+
}

‎phpunit/code/round-with-mode.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
/**
3+
* This file is part of TypePHP(AOT).
4+
*
5+
* @link https://www.swoole.com/aot/
6+
* @contact service@swoole.com
7+
*/
8+
9+
function main(): void
10+
{
11+
var_dump(round(2.5, 0, RoundingMode::HalfEven));
12+
}

‎phpunit/src/RoundModeTest.php‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
/**
3+
* This file is part of TypePHP(AOT).
4+
*
5+
* @link https://www.swoole.com/aot/
6+
* @contact service@swoole.com
7+
*/
8+
9+
namespace TypePhp\Tests;
10+
11+
use PHPUnit\Framework\TestCase;
12+
use TypePhp\CompilerTest;
13+
14+
/**
15+
* @internal
16+
* @coversNothing
17+
*/
18+
class RoundModeTest extends TestCase
19+
{
20+
public function testRoundingModeReachesTheRuntimeUnconverted(): void
21+
{
22+
global $translator;
23+
24+
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
25+
$translator = $compiler;
26+
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/round-with-mode.php';
27+
$compiler->addFiles([$source]);
28+
$compiler->prepareFile($source);
29+
$cpp = file_get_contents($compiler->convertFile($source));
30+
31+
// php::fn::round() models the mode as an Int, so an enum must not be
32+
// lowered to it: the conversion turns HalfEven into half away from
33+
// zero. The call goes to the runtime with the enum intact instead.
34+
self::assertStringNotContainsString('php::fn::round(', $cpp);
35+
self::assertStringContainsString('php::constant(', $cpp);
36+
}
37+
38+
public function testLegacyIntegerModeKeepsTheNativeCall(): void
39+
{
40+
global $translator;
41+
42+
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
43+
$translator = $compiler;
44+
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/round-with-legacy-mode.php';
45+
$compiler->addFiles([$source]);
46+
$compiler->prepareFile($source);
47+
$cpp = file_get_contents($compiler->convertFile($source));
48+
49+
self::assertStringContainsString('php::fn::round(', $cpp);
50+
}
51+
}

‎src/Optimizer/FuncCallOptimizer.php‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ protected function genArrayKeyExists(string $n, Node\Expr\FuncCall $e, array $c)
792792
return $array . '.offsetExists(' . $key . ')';
793793
}
794794

795-
protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string
795+
protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string|false
796796
{
797797
$type = $this->detectTypeOfExpr($e->args[0]->value);
798798
if ($type === Type::DECIMAL) {
@@ -804,6 +804,13 @@ protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string
804804
}
805805
$args = count($e->args);
806806
if ($args >= 3) {
807+
// php::fn::round() models the mode as an Int, which covers only the
808+
// legacy PHP_ROUND_* constants. Since PHP 8.4 the parameter is a
809+
// RoundingMode enum: converting it to an int picks a different mode,
810+
// so anything that is not statically an int goes to the runtime.
811+
if ($this->detectTypeOfExpr($e->args[2]->value) !== Type::INT) {
812+
return false;
813+
}
807814
return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ', ' . $this->convertIntExpr($this->getArg($e, 2)) . ')';
808815
}
809816
if ($args >= 2) {

0 commit comments

Comments
 (0)