diff --git a/phpunit/src/UniversalMethodCallTest.php b/phpunit/src/UniversalMethodCallTest.php index b1e69007..68d3b20b 100644 --- a/phpunit/src/UniversalMethodCallTest.php +++ b/phpunit/src/UniversalMethodCallTest.php @@ -21,4 +21,5 @@ class UniversalMethodCallTest extends \BaseTest { $this->exec('Cannot call method on void', 'void-method-call.php'); } + } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 9aed496f..9e25d66a 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -76,8 +76,10 @@ class CompilerBase extends \PhpAot\Core\Translator public const string TYPE_BOX = 'php::Box'; /** - * to* conversion methods are keywords with mandated return types. */ - public const array TO_METHOD_TYPE_MAP = [ + * Keyword methods (to* builtins) with mandated return types. + * Use findKeywordMethod() for unified lookup including keyword extension methods. + */ + public const array KEYWORD_METHOD_MAP = [ 'toInt' => self::TYPE_INT, 'toFloat' => self::TYPE_FLOAT, 'toString' => self::TYPE_STR, @@ -2519,9 +2521,10 @@ class CompilerBase extends \PhpAot\Core\Translator case 'Expr_MethodCall': if ($this->isNamedMethod($expr->name)) { $method = $this->parseIdentifier($expr->name); - // to* methods are keywords — their return type is mandated regardless of receiver - if (isset(self::TO_METHOD_TYPE_MAP[$method])) { - return self::TO_METHOD_TYPE_MAP[$method]; + // keyword methods (to* builtins + __ extensions) — return type is known regardless of receiver + $kwType = $this->findKeywordMethod($method); + if ($kwType !== null) { + return $kwType; } // Class definition resolution (handles this_, typed VarExpr) $classDef = $this->resolveObjectClassDef($expr->var); @@ -5444,7 +5447,7 @@ class CompilerBase extends \PhpAot\Core\Translator // to* conversion methods are language keywords — always dispatched directly if ($this->isNamedMethod($expr->name)) { $methodName = $expr->name->toString(); - if (isset(self::TO_METHOD_TYPE_MAP[$methodName])) { + if (isset(self::KEYWORD_METHOD_MAP[$methodName])) { if ($this->isVarExpr($expr->var)) { $receiverType = $this->getVarType($object); } else { @@ -5458,6 +5461,19 @@ class CompilerBase extends \PhpAot\Core\Translator } return $this->genToConvertCall($object, $methodName, $receiverType); } + // keyword extension methods (__ functions) — dispatched before type-specific logic + $kwExt = $this->findKeywordExtensionMethod($methodName); + if ($kwExt) { + if ($this->isVarExpr($expr->var)) { + $receiverType = $this->getVarType($object); + } else { + $receiverType = $this->detectTypeOfExpr($expr->var); + } + if ($receiverType === self::TYPE_VOID) { + $this->fatalError($expr->var, 'Cannot call method on void'); + } + return $this->parseUniversalMethodCall($expr, $object, $methodName, $kwExt, $this->isVarExpr($expr->var)); + } } // 可转为原生调用的 MethodCall diff --git a/src/Php/UniversalMethodCall.php b/src/Php/UniversalMethodCall.php index 53fa712d..f713afb8 100644 --- a/src/Php/UniversalMethodCall.php +++ b/src/Php/UniversalMethodCall.php @@ -428,6 +428,65 @@ trait UniversalMethodCall return null; } + /** + * Look up a keyword extension method: function __snake_case in root namespace + * whose first parameter is mixed/any. Callable as $receiver->camelCase() on any type. + */ + protected function findKeywordExtensionMethod(string $method): ?array + { + $snakeMethod = $this->camelToSnake($method); + $funcName = '__' . $snakeMethod; + + if (!$this->hasFunction($funcName)) { + return null; + } + + $funcDef = $this->getFunction($funcName); + + // Must be in root namespace + if ($funcDef->namespace !== '') { + return null; + } + + // First param must be mixed/any (TYPE_VAR) + if (empty($funcDef->argInfoList)) { + return null; + } + $firstParam = $funcDef->argInfoList[0]; + if ($firstParam->type !== CompilerBase::TYPE_VAR) { + return null; + } + + $totalParams = count($funcDef->argInfoList); + $minArgs = max(0, $funcDef->argCountRequired - 1); + $maxArgs = $totalParams - 1; + if ($funcDef->hasVariadicArg()) { + $maxArgs = -1; + } + + return [ + 'handler' => 'php_fn', + 'fn' => $funcName, + 'receiver_pos' => 1, + 'return_type' => $funcDef->returnType, + 'min_args' => $minArgs, + 'max_args' => $maxArgs, + ]; + } + + /** + * Unified keyword method lookup (to* builtins + __ extension methods). + * Returns the return type string, or null if the method is not a keyword method. + */ + protected function findKeywordMethod(string $method): ?string + { + if (isset(CompilerBase::KEYWORD_METHOD_MAP[$method])) { + return CompilerBase::KEYWORD_METHOD_MAP[$method]; + } + $kwExt = $this->findKeywordExtensionMethod($method); + return $kwExt ? $kwExt['return_type'] : null; + } + private function buildInternalExtensionMethod(string $type, string $funcName): ?array { $ref = Reflection::getFunction($funcName);