Skip to content
Merged
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
31 changes: 31 additions & 0 deletions phpunit/code/func-call-optimizer-typed-arguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

function optimizerDynamicBool(): mixed
{
return true;
}

function optimizerTypedBool(): bool
{
return true;
}

function optimizerTypedInt(): int
{
return 1;
}

function optimizerTypedFloat(): float
{
return 0.0;
}

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

use TypePhp\CompilerTest;

final class FuncCallOptimizerTest extends BaseTest
{
public function testStrictTypedArgumentsUseOnlyProvenDirectAbiPaths(): 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(1, substr_count($code, 'php::fn::in_array('));
self::assertSame(1, substr_count($code, 'php::fn::hypot('));
self::assertSame(1, substr_count($code, 'php::fn::json_decode('));
self::assertSame(3, substr_count($code, 'php::call('));
self::assertStringContainsString('php_optimizertypedbool()', $code);
self::assertStringContainsString('php_optimizertypedint()', $code);
self::assertStringContainsString('php_optimizertypedfloat()', $code);
self::assertStringContainsString('php_optimizerdynamicbool()', $code);
self::assertMatchesRegularExpression('/php_optimizertypedbool\(\);\s*php::fn::in_array/', $code);
self::assertMatchesRegularExpression('/php_optimizertypedint\(\);\s*php::call/', $code);
self::assertMatchesRegularExpression('/php_optimizerdynamicbool\(\);\s*php::call/', $code);
self::assertStringContainsString('php::fn::hypot(php::toFloat(', $code);
self::assertStringContainsString('php::ArgList{php::null}', $code);
self::assertMatchesRegularExpression('/php::fn::json_decode\([^;]+php::null\);/', $code);
}
}
110 changes: 104 additions & 6 deletions src/Optimizer/FuncCallOptimizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,19 @@ 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,
$nullables,
)) {
return false;
}

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

Expand All @@ -300,11 +310,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,
array $nullables,
): bool
{
// The optimized ABI conversions are safe for exact types and for
// strict PHP's int-to-float widening. Every other conversion would
// erase the runtime zval type before Zend can validate the parameter,
// so keep those calls on php::call().
$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;
}
if ($this->isNull($arg->value)) {
if ($nullables[$index] ?? false) {
continue;
}
return false;
}
$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 ($expected === Type::FLOAT && $actual === Type::INT) {
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'] ?? ''),
$refInfo['nullables'] ?? [],
);
}

// =========================================================================
// Auto-detect argument types from PHP reflection
// =========================================================================
Expand Down Expand Up @@ -845,6 +924,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 @@ -859,16 +941,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 @@ -878,6 +963,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 @@ -888,7 +976,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 @@ -913,6 +1001,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 @@ -923,6 +1015,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 @@ -984,8 +1079,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
2 changes: 1 addition & 1 deletion tests/compiler/array/array-merge-unpack.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Dg {
$this->r[] = [
'bid' => time(),
'ct' => 1,
'cid' => random_int(1e8, 2147483647),
'cid' => random_int(100000000, 2147483647),
'cp' => strtoupper(substr(bin2hex(random_bytes(8)), 0, 12)),
'crt' => date('Y-m-d H:i:s'),
'mem' => '',
Expand Down
88 changes: 53 additions & 35 deletions tests/compiler/stdlib/null_optional_arg.phpt
Original file line number Diff line number Diff line change
@@ -1,52 +1,70 @@
--TEST--
Passing null to optional parameters should use C++ default values
Nullable and non-nullable builtin parameters preserve strict null semantics
--FILE--
<?php
error_reporting(E_ALL & ~E_DEPRECATED);
declare(strict_types=1);

// substr: null length should take rest of string, not return empty
$s = 'hello world';
var_dump(substr($s, 6, null));
var_dump(substr($s, 6) === substr($s, 6, null));
var_dump(substr($s, 0, null) === $s);
var_dump(substr($s, null) === $s);
function main()
{
$s = 'hello world';
var_dump(substr($s, 6, null));
var_dump(substr($s, 6) === substr($s, 6, null));
var_dump(substr($s, 0, null) === $s);

// strpos: null offset should default to 0
var_dump(strpos($s, 'o', null));
var_dump(strpos($s, 'o', null) === strpos($s, 'o'));
try {
substr($s, null);
echo "substr-offset-null=missing TypeError\n";
} catch (TypeError $error) {
echo "substr-offset-null=TypeError\n";
}

// stripos: null offset should default to 0
var_dump(stripos($s, 'O', null));
var_dump(stripos($s, 'O', null) === stripos($s, 'O'));
try {
strpos($s, 'o', null);
echo "strpos-offset-null=missing TypeError\n";
} catch (TypeError $error) {
echo "strpos-offset-null=TypeError\n";
}

// strrpos: null offset should default to 0 (search from end)
var_dump(strrpos('hello hello', 'o', null));
var_dump(strrpos('hello hello', 'o', null) === strrpos('hello hello', 'o'));
try {
stripos($s, 'O', null);
echo "stripos-offset-null=missing TypeError\n";
} catch (TypeError $error) {
echo "stripos-offset-null=TypeError\n";
}

// strstr: null before_needle should default to false
var_dump(strstr($s, 'o', null));
var_dump(strstr($s, 'o', null) === strstr($s, 'o'));
try {
strrpos('hello hello', 'o', null);
echo "strrpos-offset-null=missing TypeError\n";
} catch (TypeError $error) {
echo "strrpos-offset-null=TypeError\n";
}

// str_repeat: ensure non-null still works
var_dump(str_repeat('ab', 3));
try {
strstr($s, 'o', null);
echo "strstr-before-needle-null=missing TypeError\n";
} catch (TypeError $error) {
echo "strstr-before-needle-null=TypeError\n";
}

// explode with null limit: null coerces to 0 (limit=0: whole string as single element)
$arr = explode(' ', 'a b c d', null);
var_dump(count($arr));
var_dump(str_repeat('ab', 3));

try {
explode(' ', 'a b c d', null);
echo "explode-limit-null=missing TypeError\n";
} catch (TypeError $error) {
echo "explode-limit-null=TypeError\n";
}
}

?>
--EXPECT--
string(5) "world"
bool(true)
bool(true)
bool(true)
int(4)
bool(true)
int(4)
bool(true)
int(10)
bool(true)
string(7) "o world"
bool(true)
substr-offset-null=TypeError
strpos-offset-null=TypeError
stripos-offset-null=TypeError
strrpos-offset-null=TypeError
strstr-before-needle-null=TypeError
string(6) "ababab"
int(1)
explode-limit-null=TypeError
Loading
Loading