From 93909274e244ec2bf977bb207f0e89c7c6b7c3e4 Mon Sep 17 00:00:00 2001 From: Giandonn Date: Sat, 29 Aug 2026 20:17:38 -0300 Subject: [PATCH] fix(optimizer): intval() with a base must not drop the base The four scalar conversions are lowered to a single-argument Native cast by dispatchConversion, which reads args[0] and ignores the rest. intval() takes a $base as its second argument, so it was silently discarded: intval("ff", 16); // php::toInt("ff") -> 0, PHP gives 255 intval("0x1A", 16); // php::toInt("0x1A") -> 0, PHP gives 26 intval("101", 2); // php::toInt("101") -> 101, PHP gives 5 Nothing reports the loss: the program compiles clean and the number is simply wrong, which is easy to miss in the code that most often uses a base - parsing hex colors, permission masks and binary flags. A conversion call with any arity other than one now falls through to the dynamic path, where both arguments are passed to the runtime function. Single-argument intval(), strval(), floatval() and boolval() keep their Native cast, so the common case is unchanged. type_conv.phpt gains the base cases, with a literal and a variable base; none of them were covered anywhere in the test suite. ConversionArityTest pins the lowering decision in the generated C++. --- phpunit/code/intval-single-argument.php | 15 +++++++ phpunit/code/intval-with-base.php | 15 +++++++ phpunit/src/ConversionArityTest.php | 52 +++++++++++++++++++++++++ src/Optimizer/FuncCallOptimizer.php | 8 +++- tests/compiler/stdlib/type_conv.phpt | 13 +++++++ 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 phpunit/code/intval-single-argument.php create mode 100644 phpunit/code/intval-with-base.php create mode 100644 phpunit/src/ConversionArityTest.php diff --git a/phpunit/code/intval-single-argument.php b/phpunit/code/intval-single-argument.php new file mode 100644 index 00000000..269416b4 --- /dev/null +++ b/phpunit/code/intval-single-argument.php @@ -0,0 +1,15 @@ +compileToCpp('intval-with-base.php'); + + // The Native cast carries no base, so both calls must keep the second + // argument by going through the dynamic path. + self::assertStringNotContainsString('php::toInt(', $cpp); + self::assertSame(2, substr_count($cpp, '16L')); + } + + public function testSingleArgumentConversionsStillLowerToNativeCasts(): void + { + $cpp = $this->compileToCpp('intval-single-argument.php'); + + self::assertStringContainsString('php::toInt(', $cpp); + self::assertStringContainsString('php::toString(', $cpp); + self::assertStringContainsString('php::toFloat(', $cpp); + self::assertStringContainsString('php::toBool(', $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/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index fe2ae690..419a2f45 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -527,8 +527,14 @@ trait FuncCallOptimizer return $target . '(' . implode(', ', $args) . ')'; } - protected function dispatchConversion(Node\Expr\FuncCall $expr, string $convType): string + protected function dispatchConversion(Node\Expr\FuncCall $expr, string $convType): string|false { + // These four are lowered as single-argument Native casts, which cannot + // carry intval()'s $base. Any other arity must reach the runtime + // function instead of silently dropping the extra argument. + if (count($expr->args) !== 1) { + return false; + } $arg = $expr->args[0]->value; $type = $this->detectTypeOfExpr($arg); $nativeClass = $this->detectClassOfExpr($arg); diff --git a/tests/compiler/stdlib/type_conv.phpt b/tests/compiler/stdlib/type_conv.phpt index a71656c6..dcc7ddbd 100644 --- a/tests/compiler/stdlib/type_conv.phpt +++ b/tests/compiler/stdlib/type_conv.phpt @@ -18,6 +18,14 @@ function main() { var_dump(intval(true)); var_dump(intval(false)); + // intval with an explicit base + var_dump(intval("ff", 16)); + var_dump(intval("0x1A", 16)); + var_dump(intval("101", 2)); + var_dump(intval("777", 8)); + $base = 16; + var_dump(intval("ff", $base)); + // floatval var_dump(floatval(42)); var_dump(floatval("3.14")); @@ -44,6 +52,11 @@ int(42) int(3) int(1) int(0) +int(255) +int(26) +int(5) +int(511) +int(255) float(42) float(3.14) float(42)