diff --git a/src/Php/UniversalMethodCall.php b/src/Php/UniversalMethodCall.php index 8850a115..3eda7c3a 100644 --- a/src/Php/UniversalMethodCall.php +++ b/src/Php/UniversalMethodCall.php @@ -173,7 +173,6 @@ trait UniversalMethodCall 'jsonDecodeToObject' => ['handler' => 'php_fn', 'fn' => 'json_decode', 'return_type' => self::TYPE_OBJECT, 'min_args' => 0, 'max_args' => 2, 'const_args' => [1 => 'false']], // phpx C++ methods (no PHP function equivalent) 'equals' => ['handler' => 'direct_method', 'method' => 'equals', 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 2], - 'append' => ['handler' => 'direct_method_mutate', 'method' => 'append', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 1], // conversions 'toInt' => ['handler' => 'convert_fn', 'fn' => 'toInt', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], 'toFloat' => ['handler' => 'convert_fn', 'fn' => 'toFloat', 'return_type' => self::TYPE_FLOAT, 'min_args' => 0, 'max_args' => 0], @@ -400,6 +399,9 @@ trait UniversalMethodCall /** * Look up an extension function for the given type+method. * Extension functions follow the naming convention: {typePrefix}_{snake_case_method} + * + * Checks compiled user-defined functions first, then falls back to PHP internal + * functions using reflection to resolve parameter counts and return types. */ protected function findExtensionMethod(string $type, string $method): ?array { @@ -409,25 +411,116 @@ trait UniversalMethodCall } $snakeMethod = $this->camelToSnake($method); - $baseName = $prefix . '_' . $snakeMethod; + $funcName = $prefix . '_' . $snakeMethod; - $funcName = $this->resolveExtensionFunctionName($baseName); - if ($funcName === null) { + $resolvedName = $this->resolveExtensionFunctionName($funcName); + if ($resolvedName !== null) { + $funcDef = $this->getFunction($resolvedName); + if (!$this->validateExtensionFirstParam($type, $funcDef)) { + return null; + } + return [ + 'handler' => 'php_fn', + 'fn' => $resolvedName, + 'receiver_pos' => 1, + 'return_type' => $funcDef->returnType, + 'min_args' => 0, + 'max_args' => -1, + ]; + } + + if ($this->isInternalFunction($funcName)) { + return $this->buildInternalExtensionMethod($type, $funcName); + } + + return null; + } + + private function buildInternalExtensionMethod(string $type, string $funcName): ?array + { + $ref = Reflection::getFunction($funcName); + if ($ref === null) { return null; } - $funcDef = $this->getFunction($funcName); + $totalParams = $ref->getNumberOfParameters(); + if ($totalParams < 1) { + return null; + } + + if (!$this->validateInternalExtensionFirstParam($type, $funcName)) { + return null; + } + + $phpType = Reflection::getFunctionReturnType($funcName); + $returnType = $phpType ? ($this->zendTypeMap[$phpType] ?? self::TYPE_VAR) : self::TYPE_VAR; + + $requiredParams = $ref->getNumberOfRequiredParameters(); + $minArgs = max(0, $requiredParams - 1); + $maxArgs = max(0, $totalParams - 1); + + if ($totalParams > 0) { + $lastParam = Reflection::getFunctionParameter($funcName, $totalParams - 1); + if ($lastParam !== null && $lastParam->isVariadic()) { + $maxArgs = -1; + } + } return [ 'handler' => 'php_fn', 'fn' => $funcName, 'receiver_pos' => 1, - 'return_type' => $funcDef->returnType, - 'min_args' => 0, - 'max_args' => -1, + 'return_type' => $returnType, + 'min_args' => $minArgs, + 'max_args' => $maxArgs, ]; } + private function validateExtensionFirstParam(string $type, \PhpAot\Php\Entity\FunctionDef $funcDef): bool + { + if (empty($funcDef->argInfoList)) { + return false; + } + $firstParam = $funcDef->argInfoList[0]; + // Stream is a PHP pseudo-type; its params are always untyped (TYPE_VAR) or references + if ($type === self::TYPE_STREAM) { + return $firstParam->byRef || $firstParam->type === self::TYPE_VAR || $firstParam->type === self::TYPE_REF; + } + if ($firstParam->byRef) { + return $type === self::TYPE_ARRAY; + } + $paramType = $firstParam->type; + if ($paramType === self::TYPE_VAR) { + return false; + } + return $paramType === $type; + } + + private function validateInternalExtensionFirstParam(string $type, string $funcName): bool + { + $param = Reflection::getFunctionParameter($funcName, 0); + if ($param === null) { + return false; + } + // Stream pseudo-type: accept untyped or by-reference first params + if ($type === self::TYPE_STREAM) { + return true; + } + if ($param->isPassedByReference()) { + return $type === self::TYPE_ARRAY; + } + $paramType = $param->getType(); + if ($paramType === null) { + return false; + } + if ($paramType instanceof \ReflectionNamedType) { + $phpName = $paramType->getName(); + $compilerType = $this->zendTypeMap[$phpName] ?? null; + return $compilerType === $type; + } + return false; + } + protected function resolveExtensionFunctionName(string $funcName): ?string { if ($this->hasFunction($funcName)) { diff --git a/tests/aot/string_method/misc.phpt b/tests/aot/string_method/misc.phpt index ac2102bf..b7c5a315 100644 --- a/tests/aot/string_method/misc.phpt +++ b/tests/aot/string_method/misc.phpt @@ -86,11 +86,6 @@ function main() var_dump($n2->isNumeric()); var_dump($n3->isNumeric()); - // append (mutating) - $s = "hello"; - $s->append(" world"); - var_dump($s); - // substr $s = "hello world"; var_dump($s->substr(6)); @@ -169,7 +164,6 @@ bool(true) bool(true) bool(true) bool(false) -string(11) "hello world" string(5) "world" string(5) "hello" string(5) "world" diff --git a/tests/aot/universal_method_internal.phpt b/tests/aot/universal_method_internal.phpt new file mode 100644 index 00000000..5eca4edc --- /dev/null +++ b/tests/aot/universal_method_internal.phpt @@ -0,0 +1,19 @@ +--TEST-- +Universal method: PHP internal function as extension method via reflection +--FILE-- +rot13()); + var_dump($str->rot13()->upper()); + var_dump($str->rot13()->length()); +} +?> +--EXPECT-- +string(5) "uryyb" +string(5) "URYYB" +int(5)