fix(compiler): support first-class callable of internal functions (#93)

* fix(compiler): support first-class callable of internal functions

PHP 8.1 `foo(...)` builds a Closure from a single VariadicPlaceholder
argument instead of calling foo. Two paths mishandled it:

- checkInternalFunctionArgCount() counted the placeholder as a real
  argument, so first-class callables of internal functions requiring 2+
  parameters (e.g. `posix_kill(...)`) failed with a bogus "expects at
  least N arguments, 1 given".
- The math-function return-type optimization read `$expr->args[0]->value`
  on callables such as `round(...)`, crashing on the undefined
  VariadicPlaceholder::$value property.

Guard both with isFirstClassCallable() so the expression falls through to
Closure (Type::OBJECT) resolution.

* fix(compiler): infer builtin callables before return optimizations

---------

Co-authored-by: Tinywan <tinywan@gmail.com>
master
ShaoBo Wan(無尘) 1 month ago committed by GitHub
parent 38d828cbbe
commit 055dee01f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      phpunit/src/SsaAnalysisTest.php
  2. 15
      src/CompilerBase.php
  3. 19
      tests/compiler/place-holder/builtin-multi-arg-callable.phpt
  4. 16
      tests/compiler/place-holder/math-function-callable.phpt
  5. 14
      tests/compiler/place-holder/stream-function-callable.phpt

@ -1345,6 +1345,18 @@ class SsaAnalysisTest extends TestCase
}
}
public function testDetectTypeOfFirstClassCallableBeforeBuiltinReturnTypeOptimizations(): void
{
foreach (['round', 'fopen'] as $function) {
$call = new Expr\FuncCall(
new Node\Name($function),
[new Node\VariadicPlaceholder()]
);
$this->assertSame(Type::OBJECT, $this->invoke('detectTypeOfExpr', $call));
}
}
public function testDetectTypeOfConcatExpressions(): void
{
$concat = new Expr\BinaryOp\Concat(new Scalar\LNumber(1), new Scalar\String_(''));

@ -3148,11 +3148,15 @@ class CompilerBase implements PropertyAccessContext
}
break;
case 'Expr_FuncCall':
if ($expr->isFirstClassCallable()) {
return Type::OBJECT;
}
if ($this->isNameExpr($expr->name)) {
$name = $this->parseIdentifier($expr->name);
$globalName = ltrim($name, '\\');
// Math function optimization: propagate Big* return types
if (in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true) && !empty($expr->args)) {
if (in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true)
&& !empty($expr->args)) {
$argType = $this->detectTypeOfExpr($expr->args[0]->value);
if (
$argType === Type::BIGINT
@ -3176,9 +3180,6 @@ class CompilerBase implements PropertyAccessContext
if (in_array($name, self::STREAM_FUNCTIONS)) {
return Type::STREAM;
}
if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) {
return Type::OBJECT;
}
if ($this->hasFunction($name)) {
return $this->getFunction($name)->returnType;
}
@ -3467,6 +3468,12 @@ class CompilerBase implements PropertyAccessContext
if ($this->hasUnpackCallArg($expr->args)) {
return;
}
// `foo(...)` is PHP 8.1 first-class callable syntax: it creates a
// Closure instead of calling foo, so its single VariadicPlaceholder
// must not be counted/validated against foo's real signature.
if ($expr->isFirstClassCallable()) {
return;
}
$actualArgCount = count($expr->args);
$config = $this->getFuncCallConfig()[ltrim($funcName, '\\')] ?? null;
$allowedArgCounts = is_array($config) ? ($config['argCounts'] ?? null) : null;

@ -0,0 +1,19 @@
--TEST--
First-class callable of builtins requiring multiple arguments
--FILE--
<?php
function main(): void {
// str_repeat() and array_slice() are internal functions with 2 required
// parameters. `foo(...)` builds a Closure; its single VariadicPlaceholder
// must not be counted as one real argument and rejected by the internal
// function argument-count check.
$repeat = str_repeat(...);
echo $repeat('ab', 3), "\n";
$slice = array_slice(...);
echo implode(',', $slice([1, 2, 3, 4, 5], 1, 2)), "\n";
}
?>
--EXPECT--
ababab
2,3

@ -0,0 +1,16 @@
--TEST--
First-class callable of a math function (round)
--FILE--
<?php
function main(): void {
// round() is in the math-function return-type optimization list. For the
// first-class callable `round(...)`, args[0] is a VariadicPlaceholder with
// no ->value, so the optimization must be skipped and the expression must
// resolve to a Closure instead of crashing.
$values = [1.4, 2.6, 3.5];
$rounded = array_map(round(...), $values);
echo implode(',', $rounded), "\n";
}
?>
--EXPECT--
1,3,4

@ -0,0 +1,14 @@
--TEST--
First-class callable of a stream-returning function
--FILE--
<?php
function useOpener(Closure $open): void {
echo "callable\n";
}
function main(): void {
useOpener(fopen(...));
}
?>
--EXPECT--
callable
Loading…
Cancel
Save