From c291d79703726d4f053025910df024fdd8d9842c Mon Sep 17 00:00:00 2001 From: Giandonn Date: Sun, 30 Aug 2026 10:52:42 -0300 Subject: [PATCH] fix(optimizer): keep unpacked and named conversion arguments dynamic An unpacked call carries a single Node\Arg regardless of its runtime arity, so the arity check alone still accepted intval(...$args) as a single-argument Native conversion and lowered the array itself: intval(...['ff', 16]) // php::toInt(withBase) -> int(1) strval(...['42']) // php::toString(single) -> "Array" + warning A named argument has the same shape and need not be the value being converted: intval(bogus: 1) must raise "Unknown named parameter", not fold to a cast of 1. Reject both in dispatchConversion() so the runtime determines the expanded arity and the parameter names, matching what dispatchFuncCall() already does for every other builtin. --- phpunit/code/intval-unpacked-argument.php | 21 +++++++++++++++++++++ phpunit/src/ConversionArityTest.php | 15 +++++++++++++++ src/Optimizer/FuncCallOptimizer.php | 11 ++++++++++- tests/compiler/stdlib/type_conv.phpt | 22 ++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 phpunit/code/intval-unpacked-argument.php diff --git a/phpunit/code/intval-unpacked-argument.php b/phpunit/code/intval-unpacked-argument.php new file mode 100644 index 00000000..8bd5aa69 --- /dev/null +++ b/phpunit/code/intval-unpacked-argument.php @@ -0,0 +1,21 @@ +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'); diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 419a2f45..60f86c7f 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -532,7 +532,16 @@ trait FuncCallOptimizer // 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) { + // + // 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; diff --git a/tests/compiler/stdlib/type_conv.phpt b/tests/compiler/stdlib/type_conv.phpt index dcc7ddbd..3bbfba40 100644 --- a/tests/compiler/stdlib/type_conv.phpt +++ b/tests/compiler/stdlib/type_conv.phpt @@ -26,6 +26,21 @@ function main() { $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")); @@ -57,6 +72,13 @@ 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)