From 9ad213ea6438de40d4d67021011a085edead256f Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 31 Aug 2026 10:50:45 +0800 Subject: [PATCH] fix(optimizer): preserve unpacked call semantics --- phpunit/code/count-literal-fold-unpack.php | 21 +++++++++++ phpunit/code/func-call-optimizer-unpack.php | 22 ++++++++++++ phpunit/src/CountLiteralFoldTest.php | 8 +++++ phpunit/src/FuncCallOptimizerUnpackTest.php | 36 +++++++++++++++++++ src/Optimizer/FuncCallOptimizer.php | 6 +++- tests/compiler/array/count-literal-fold.phpt | 16 +++++++++ .../stdlib/optimized-call-unpack.phpt | 28 +++++++++++++++ 7 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 phpunit/code/count-literal-fold-unpack.php create mode 100644 phpunit/code/func-call-optimizer-unpack.php create mode 100644 phpunit/src/FuncCallOptimizerUnpackTest.php create mode 100644 tests/compiler/stdlib/optimized-call-unpack.phpt diff --git a/phpunit/code/count-literal-fold-unpack.php b/phpunit/code/count-literal-fold-unpack.php new file mode 100644 index 00000000..8db2803f --- /dev/null +++ b/phpunit/code/count-literal-fold-unpack.php @@ -0,0 +1,21 @@ +getMessage(), "\n"; + } +} diff --git a/phpunit/code/func-call-optimizer-unpack.php b/phpunit/code/func-call-optimizer-unpack.php new file mode 100644 index 00000000..78a94046 --- /dev/null +++ b/phpunit/code/func-call-optimizer-unpack.php @@ -0,0 +1,22 @@ + 1]]; + $functionExistsArgs = ['strlen']; + + var_dump(intval(...$intvalArgs)); + var_dump(round(...$roundArgs)); + var_dump(is_null(...$nullArgs)); + var_dump(array_keys(...$arrayKeysArgs)); + var_dump(function_exists(...$functionExistsArgs)); +} diff --git a/phpunit/src/CountLiteralFoldTest.php b/phpunit/src/CountLiteralFoldTest.php index 062a0ea9..2be3afea 100644 --- a/phpunit/src/CountLiteralFoldTest.php +++ b/phpunit/src/CountLiteralFoldTest.php @@ -37,6 +37,14 @@ class CountLiteralFoldTest extends TestCase self::assertStringNotContainsString('php::fn::count(', $cpp); } + public function testArgumentUnpackingUsesTheRuntimeCallPath(): void + { + $cpp = $this->compileToCpp('count-literal-fold-unpack.php'); + + self::assertSame(3, substr_count($cpp, '.appendUnpacked(')); + self::assertStringNotContainsString('php::fn::count(', $cpp); + } + private function compileToCpp(string $file): string { global $translator; diff --git a/phpunit/src/FuncCallOptimizerUnpackTest.php b/phpunit/src/FuncCallOptimizerUnpackTest.php new file mode 100644 index 00000000..f366b07f --- /dev/null +++ b/phpunit/src/FuncCallOptimizerUnpackTest.php @@ -0,0 +1,36 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $cpp = file_get_contents($compiler->convertFile($source)); + + self::assertSame(5, substr_count($cpp, '.appendUnpacked(')); + self::assertStringNotContainsString('php::toInt(', $cpp); + self::assertStringNotContainsString('php::fn::round(', $cpp); + self::assertStringNotContainsString('php::fn::array_keys(', $cpp); + } +} diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index d76e5ae1..37fe4a0d 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -203,7 +203,11 @@ trait FuncCallOptimizer if ($this->isPlaceholderExpr($arg)) { return false; } - if ($arg instanceof Node\Arg && $arg->name !== null) { + // Custom handlers, big-type dispatch and scalar conversions work + // with the syntactic argument list. Named arguments and unpacking + // require Zend's runtime binding/expansion semantics, so reject + // them before any optimizer-specific handler can consume them. + if ($arg instanceof Node\Arg && ($arg->name !== null || $arg->unpack)) { return false; } } diff --git a/tests/compiler/array/count-literal-fold.phpt b/tests/compiler/array/count-literal-fold.phpt index d8552362..6f370bf5 100644 --- a/tests/compiler/array/count-literal-fold.phpt +++ b/tests/compiler/array/count-literal-fold.phpt @@ -72,6 +72,19 @@ function main() var_dump(count([1.5, 'text', true, false, null])); var_dump(count([-2, +3, -1.5])); var_dump(count([])); + + // Argument unpacking must happen before count() receives its arguments. + $countArgs = [[1, 2, 3]]; + var_dump(count(...$countArgs)); + var_dump(count(...[[1, 2, 3]])); + + // An empty unpack must retain Zend's argument-count validation. + try { + var_dump(count(...[])); + echo "argument-count-error-not-thrown\n"; + } catch (ArgumentCountError $e) { + echo "caught=", $e->getMessage(), "\n"; + } } ?> --EXPECT-- @@ -93,3 +106,6 @@ int(2) int(5) int(3) int(0) +int(3) +int(3) +caught=count() expects at least 1 argument, 0 given diff --git a/tests/compiler/stdlib/optimized-call-unpack.phpt b/tests/compiler/stdlib/optimized-call-unpack.phpt new file mode 100644 index 00000000..0bbd06d0 --- /dev/null +++ b/tests/compiler/stdlib/optimized-call-unpack.phpt @@ -0,0 +1,28 @@ +--TEST-- +optimized builtin calls preserve argument unpacking +--FILE-- + 1]]; + $functionExistsArgs = ['strlen']; + + var_dump(intval(...$intvalArgs)); + var_dump(round(...$roundArgs)); + var_dump(is_null(...$nullArgs)); + var_dump(array_keys(...$arrayKeysArgs)); + var_dump(function_exists(...$functionExistsArgs)); +} +?> +--EXPECT-- +int(255) +float(2) +bool(true) +array(1) { + [0]=> + string(1) "a" +} +bool(true)