Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions phpunit/code/func-call-optimizer-typed-arguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

function optimizerDynamicBool(): mixed
{
return true;
}

function optimizerTypedBool(): bool
{
return true;
}

function optimizerTypedArgumentCalls(): void
{
in_array('1', [1], 1);
in_array('1', [1], optimizerTypedBool());
in_array('1', [1], optimizerDynamicBool());
}
27 changes: 27 additions & 0 deletions phpunit/src/FuncCallOptimizerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use TypePhp\CompilerTest;

final class FuncCallOptimizerTest extends BaseTest
{
public function testRuntimeBackedTypedArgumentsFallbackWithoutDisablingStaticConversions(): void
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/func-call-optimizer-typed-arguments.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);

self::assertIsString($code);
self::assertSame(2, substr_count($code, 'php::fn::in_array('));
self::assertSame(1, substr_count($code, 'php::call('));
self::assertMatchesRegularExpression('/php::toBool\(1L+\)/', $code);
self::assertStringContainsString('php_optimizertypedbool()', $code);
self::assertStringContainsString('php_optimizerdynamicbool()', $code);
self::assertStringNotContainsString('php::toBool(php_optimizerdynamicbool())', $code);
}
}
105 changes: 99 additions & 6 deletions src/Optimizer/FuncCallOptimizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,14 @@ protected function dispatchFuncCall(string $name, Node\Expr\FuncCall $expr, arra
$refInfo = $this->getArgReflectionInfo($name);
$argTypeStr = $config['args'] ?? ($refInfo['args'] ?? '');
$defaults = $config['defaults'] ?? [];
$variadicType = $config['variadicType'] ?? ($refInfo['variadicType'] ?? '');
$nullables = $refInfo['nullables'] ?? [];

if (!$this->hasOptimizerSafeTypedArguments($expr, $argTypeStr, $variadicType)) {
return false;
}

if (!empty($config['variadic']) || ($refInfo['variadic'] ?? false)) {
$variadicType = $config['variadicType'] ?? $refInfo['variadicType'] ?? '';
return $this->genVariadicCall($target, $expr, $variadicType);
}

Expand All @@ -299,11 +304,80 @@ protected function dispatchFuncCall(string $name, Node\Expr\FuncCall $expr, arra
}
}

$nullables = $refInfo['nullables'] ?? [];
$args = $this->buildArgList($expr, $argTypeStr, $defaults, $nullables);
return $target . '(' . implode(', ', $args) . ')';
}

protected function hasOptimizerSafeTypedArguments(
Node\Expr\FuncCall $expr,
string $argTypeStr,
string $variadicType
): bool
{
// The optimized ABI conversions are safe for exact types and for the
// compiler's existing statically-known Native scalar conversions. A
// runtime-backed value would lose its zval type before Zend can apply
// strict parameter validation, so keep those calls on php::call().
// Arrays have no exact argument-conversion helper and use the same
// fallback for every statically unproven value.
$types = $argTypeStr === '' ? [] : explode('_', $argTypeStr);
foreach ($expr->args as $index => $arg) {
// Custom handlers call this helper too. They cannot lower an
// unpacked list as a fixed C++ ABI argument sequence.
if ($arg->unpack) {
return false;
}
$type = $types[$index] ?? $variadicType;
$base = ($type[0] ?? '') === self::ARG_OPTIONAL ? substr($type, 1) : $type;
if (!in_array($base, [
self::ARG_TYPE_STR,
self::ARG_TYPE_INT,
self::ARG_TYPE_FLOAT,
self::ARG_TYPE_BOOL,
self::ARG_TYPE_ARRAY,
], true)) {
continue;
}
// Keep the established optimized-null policy. Several stdlib
// wrappers intentionally map literal null to their C++ default,
// including parameters that Reflection no longer marks nullable.
if ($this->isNull($arg->value)) {
continue;
}
$expected = match ($base) {
self::ARG_TYPE_STR => Type::STR,
self::ARG_TYPE_INT => Type::INT,
self::ARG_TYPE_FLOAT => Type::FLOAT,
self::ARG_TYPE_BOOL => Type::BOOL,
self::ARG_TYPE_ARRAY => Type::ARRAY,
};
$actual = $this->detectTypeOfExpr($arg->value);
if ($actual === $expected) {
continue;
}
if ($this->isNativeType($expected) && $this->isNativeType($actual)) {
continue;
}
return false;
}

return true;
}

protected function hasOptimizerSafeReflectedArguments(
string $name,
Node\Expr\FuncCall $expr,
array $config
): bool
{
$refInfo = $this->getArgReflectionInfo($name);
return $this->hasOptimizerSafeTypedArguments(
$expr,
$config['args'] ?? ($refInfo['args'] ?? ''),
$config['variadicType'] ?? ($refInfo['variadicType'] ?? ''),
);
}

// =========================================================================
// Auto-detect argument types from PHP reflection
// =========================================================================
Expand Down Expand Up @@ -768,6 +842,9 @@ protected function genGetParentClass(string $n, Node\Expr\FuncCall $e, array $c)

protected function genArrayKeys(string $n, Node\Expr\FuncCall $e, array $c): string|false
{
if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) {
return false;
}
$cnt = count($e->args);
if ($cnt >= 3) {
if ($this->detectTypeOfExpr($e->args[2]->value) !== Type::BOOL) {
Expand All @@ -782,16 +859,19 @@ protected function genArrayKeys(string $n, Node\Expr\FuncCall $e, array $c): str
return 'php::fn::array_keys(' . $this->getArg($e, 0) . ')';
}

protected function genArrayKeyExists(string $n, Node\Expr\FuncCall $e, array $c): string
protected function genArrayKeyExists(string $n, Node\Expr\FuncCall $e, array $c): string|false
{
if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) {
return false;
}
// The C++ receiver is PHP's second argument, but PHP still evaluates
// the key first. Resolve both in source order before rearranging them.
$key = $this->getArg($e, 0);
$array = $this->getArg($e, 1);
return $array . '.offsetExists(' . $key . ')';
}

protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string
protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string|false
{
$type = $this->detectTypeOfExpr($e->args[0]->value);
if ($type === Type::DECIMAL) {
Expand All @@ -801,6 +881,9 @@ protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string
}
return 'php::Decimal::round(' . $a0 . ')';
}
if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) {
return false;
}
$args = count($e->args);
if ($args >= 3) {
return 'php::fn::round(' . $this->getArg($e, 0) . ', ' . $this->convertIntExpr($this->getArg($e, 1)) . ', ' . $this->convertIntExpr($this->getArg($e, 2)) . ')';
Expand All @@ -811,7 +894,7 @@ protected function genRound(string $n, Node\Expr\FuncCall $e, array $c): string
return 'php::fn::round(' . $this->getArg($e, 0) . ')';
}

protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string
protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string|false
{
$receiver = $e->args[0] ?? null;
$nativeClass = $receiver instanceof Node\Arg
Expand All @@ -836,6 +919,10 @@ protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string
));
}

if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) {
return false;
}

$folded = $this->doFoldCountLiteral($e);
if ($folded !== false) return $folded;
if (count($e->args) >= 2) {
Expand All @@ -846,6 +933,9 @@ protected function genCount(string $n, Node\Expr\FuncCall $e, array $c): string

protected function genDefine(string $n, Node\Expr\FuncCall $e, array $c): string|false
{
if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) {
return false;
}
$arg = $e->args[0]->value;
if ($this->isScalarString($arg) && str_contains($arg->value, '::')) {
$this->fatalError($e, 'Invalid define name `' . $arg->value . '`');
Expand Down Expand Up @@ -907,8 +997,11 @@ protected function genFuncNumArgs(string $name, Node\Expr\FuncCall $expr, array
return (string) count($funcDef->argInfoList);
}

protected function genFunctionExists(string $name, Node\Expr\FuncCall $expr, array $config): string
protected function genFunctionExists(string $name, Node\Expr\FuncCall $expr, array $config): string|false
{
if (!$this->hasOptimizerSafeReflectedArguments($name, $expr, $config)) {
return false;
}
$funcName = $expr->args[0]->value;
if ($this->isScalarString($funcName)) {
$nameLower = strtolower(trim($funcName->value, '\\'));
Expand Down
Loading
Loading