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 testUnpackedAndNamedArgumentsStayOnTheDynamicPath(): void + { + $cpp = $this->compileToCpp('intval-unpacked-argument.php'); + + // An unpacked argument is one Node\Arg whatever its runtime arity is, + // so the array itself must never be handed to a Native cast. + self::assertStringNotContainsString('php::toInt(', $cpp); + self::assertStringNotContainsString('php::toString(', $cpp); + self::assertStringNotContainsString('php::toFloat(', $cpp); + self::assertStringNotContainsString('php::toBool(', $cpp); + + // Five full unpacks plus the partial intval('ff', ...[16]). + self::assertSame(6, substr_count($cpp, 'appendUnpacked(')); + } + + 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 42f98db6..967878f7 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -527,8 +527,23 @@ 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. + // + // An unpacked or named argument is a single Node\Arg whatever its + // runtime arity turns out to be, so neither may be read as the value + // being converted; both stay on the dynamic path like dispatchFuncCall() + // already does for every other builtin. + if (count($expr->args) !== 1 + || !($expr->args[0] instanceof Node\Arg) + || $expr->args[0]->unpack + || $expr->args[0]->name !== null + ) { + 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..3bbfba40 100644 --- a/tests/compiler/stdlib/type_conv.phpt +++ b/tests/compiler/stdlib/type_conv.phpt @@ -18,6 +18,29 @@ 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)); + + // An unpacked argument carries its own arity, which only the runtime + // knows, so these must not be lowered as single-argument casts. + $withBase = ["ff", 16]; + $single = ["42"]; + var_dump(intval(...$withBase)); + var_dump(intval(...$single)); + var_dump(strval(...$single)); + var_dump(floatval(...$single)); + var_dump(boolval(...$single)); + var_dump(intval("ff", ...[16])); + + // A named argument is likewise a single Arg node that does not have to + // be the value being converted. + var_dump(intval(value: "42")); + // floatval var_dump(floatval(42)); var_dump(floatval("3.14")); @@ -44,6 +67,18 @@ int(42) int(3) int(1) int(0) +int(255) +int(26) +int(5) +int(511) +int(255) +int(255) +int(42) +string(2) "42" +float(42) +bool(true) +int(255) +int(42) float(42) float(3.14) float(42)