diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 054a7b08..ff4505fd 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -2283,6 +2283,9 @@ class CompilerBase extends \PhpAot\Core\Translator if (in_array($name, self::STREAM_FUNCTIONS) || $name === 'stream_cast') { return self::TYPE_STREAM; } + if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) { + return self::TYPE_OBJECT; + } if ($this->hasFunction($name)) { return $this->getFunction($name)->returnType; } @@ -2295,6 +2298,9 @@ class CompilerBase extends \PhpAot\Core\Translator // Class definition resolution (handles this_, typed VarExpr) $classDef = $this->resolveObjectClassDef($expr->var); if ($classDef !== null && $classDef->hasMethod($method)) { + if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) { + return self::TYPE_OBJECT; + } return $classDef->getMethod($method)->getReturnType(); } if ($this->isVarExpr($expr->var)) { @@ -2321,6 +2327,10 @@ class CompilerBase extends \PhpAot\Core\Translator break; case 'Expr_StaticCall': if ($this->isNameExpr($expr->class) && $this->isIdExpr($expr->name)) { + // First-class callable syntax creates a Closure, not a method return value + if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) { + return self::TYPE_OBJECT; + } $className = $this->parseIdentifier($expr->class); if ($className === 'self') { $className = $this->getFullClassName(); diff --git a/src/Php/UniversalMethodCall.php b/src/Php/UniversalMethodCall.php index 82c86b0d..b5b148da 100644 --- a/src/Php/UniversalMethodCall.php +++ b/src/Php/UniversalMethodCall.php @@ -39,7 +39,7 @@ trait UniversalMethodCall self::TYPE_STR => [ // --- stdext string_methods (all use PHP standard functions) --- 'length' => ['handler' => 'php_fn', 'fn' => 'strlen', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], - 'isEmpty' => ['handler' => 'str_is_empty', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + 'isEmpty' => ['handler' => 'direct_method', 'method' => 'empty', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], 'lower' => ['handler' => 'php_fn', 'fn' => 'strtolower', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'upper' => ['handler' => 'php_fn', 'fn' => 'strtoupper', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'lowerFirst' => ['handler' => 'php_fn', 'fn' => 'lcfirst', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], @@ -56,7 +56,7 @@ trait UniversalMethodCall 'trim' => ['handler' => 'php_fn', 'fn' => 'trim', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 2], 'lTrim' => ['handler' => 'php_fn', 'fn' => 'ltrim', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], 'rTrim' => ['handler' => 'php_fn', 'fn' => 'rtrim', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], - 'parseStr' => ['handler' => 'php_fn', 'fn' => 'swoole_parse_str', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 0], + 'parseStr' => ['handler' => 'cpp_fn', 'fn' => 'php::fn::parse_str', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 0], 'parseUrl' => ['handler' => 'php_fn', 'fn' => 'parse_url', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 1], 'contains' => ['handler' => 'php_fn', 'fn' => 'str_contains', 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 1], 'incr' => ['handler' => 'php_fn', 'fn' => 'str_increment', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], @@ -90,16 +90,16 @@ trait UniversalMethodCall 'md5' => ['handler' => 'php_fn', 'fn' => 'md5', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], 'sha1' => ['handler' => 'php_fn', 'fn' => 'sha1', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], 'crc32' => ['handler' => 'php_fn', 'fn' => 'crc32', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], - 'hash' => ['handler' => 'php_fn', 'fn' => 'swoole_hash', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 2], - 'hashCode' => ['handler' => 'php_fn', 'fn' => 'crc32', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], + 'hash' => ['handler' => 'php_fn', 'fn' => 'hash', 'receiver_pos' => 2, 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 2], + 'hashCode' => ['handler' => 'direct_method', 'method' => 'hashCode', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], 'base64Decode' => ['handler' => 'php_fn', 'fn' => 'base64_decode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'base64Encode' => ['handler' => 'php_fn', 'fn' => 'base64_encode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'urlDecode' => ['handler' => 'php_fn', 'fn' => 'urldecode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'urlEncode' => ['handler' => 'php_fn', 'fn' => 'urlencode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'rawUrlEncode' => ['handler' => 'php_fn', 'fn' => 'rawurlencode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'rawUrlDecode' => ['handler' => 'php_fn', 'fn' => 'rawurldecode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], - 'match' => ['handler' => 'php_fn', 'fn' => 'swoole_str_match', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], - 'matchAll' => ['handler' => 'php_fn', 'fn' => 'swoole_str_match_all', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'match' => ['handler' => 'direct_method', 'method' => 'match', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 3, 'int_cast_args' => [1, 2]], + 'matchAll' => ['handler' => 'direct_method', 'method' => 'matchAll', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 3, 'int_cast_args' => [1, 2]], 'isNumeric' => ['handler' => 'php_fn', 'fn' => 'is_numeric', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], // mbstring 'mbUpperFirst' => ['handler' => 'php_fn', 'fn' => 'mb_ucfirst', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], @@ -127,8 +127,8 @@ trait UniversalMethodCall // serialize 'unserialize' => ['handler' => 'php_fn', 'fn' => 'unserialize', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], 'unmarshal' => ['handler' => 'php_fn', 'fn' => 'unserialize', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], - 'jsonDecode' => ['handler' => 'php_fn', 'fn' => 'swoole_str_json_decode', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 2], - 'jsonDecodeToObject' => ['handler' => 'php_fn', 'fn' => 'swoole_str_json_decode_to_object', 'return_type' => self::TYPE_OBJECT, 'min_args' => 0, 'max_args' => 2], + 'jsonDecode' => ['handler' => 'php_fn', 'fn' => 'json_decode', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 2, 'const_args' => [1 => 'true']], + '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], @@ -136,7 +136,6 @@ trait UniversalMethodCall '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], 'toBool' => ['handler' => 'convert_fn', 'fn' => 'toBool', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], - 'toString' => ['handler' => 'identity', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], ], self::TYPE_ARRAY => [ // --- stdext array_methods (all use PHP standard functions) --- @@ -175,7 +174,6 @@ trait UniversalMethodCall 'merge' => ['handler' => 'php_fn', 'fn' => 'array_merge', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], 'contains' => ['handler' => 'php_fn', 'fn' => 'in_array', 'receiver_pos' => 2, 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 2], 'join' => ['handler' => 'php_fn', 'fn' => 'implode', 'receiver_pos' => 2, 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 1], - 'isTyped' => ['handler' => 'php_fn', 'fn' => 'swoole_array_is_typed', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 1], 'isEmpty' => ['handler' => 'direct_method', 'method' => 'empty', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], // mutating via PHP reference functions 'sort' => ['handler' => 'php_fn_ref', 'fn' => 'sort', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 1], @@ -185,8 +183,8 @@ trait UniversalMethodCall 'unshift' => ['handler' => 'php_fn_ref', 'fn' => 'array_unshift', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => -1], 'splice' => ['handler' => 'php_fn_ref', 'fn' => 'array_splice', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 3], 'walk' => ['handler' => 'php_fn_ref', 'fn' => 'array_walk', 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 2], - 'replaceStr' => ['handler' => 'php_fn', 'fn' => 'swoole_array_replace_str', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 2], - 'iReplaceStr' => ['handler' => 'php_fn', 'fn' => 'swoole_array_ireplace_str', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 2], + 'replaceStr' => ['handler' => 'php_fn', 'fn' => 'str_replace', 'receiver_pos' => 3, 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 2], + 'iReplaceStr' => ['handler' => 'php_fn', 'fn' => 'str_ireplace', 'receiver_pos' => 3, 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 2], // serialize 'serialize' => ['handler' => 'php_fn', 'fn' => 'serialize', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], 'marshal' => ['handler' => 'php_fn', 'fn' => 'serialize', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], @@ -267,13 +265,12 @@ trait UniversalMethodCall 'calc_op' => $this->genUniversalCalcOp($receiver, $def['op'], $expr->args), 'calc_inc' => '(' . $receiver . ' + 1)', 'calc_dec' => '(' . $receiver . ' - 1)', - 'direct_method' => $this->genUniversalDirectMethod($receiver, $def['method'], $expr->args), + 'direct_method' => $this->genUniversalDirectMethod($receiver, $def['method'], $expr->args, $def['int_cast_args'] ?? []), 'direct_method_mutate' => $this->genUniversalMutatingMethod($receiver, $def['method'], $expr->args), 'convert_fn' => $this->genUniversalConvertFn($receiver, $def['fn'], $def['return_type']), - 'str_is_empty' => '(' . $receiver . '.length() == 0)', - 'identity' => $receiver, - 'php_fn' => $this->genUniversalPhpFn($receiver, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0), + 'php_fn' => $this->genUniversalPhpFn($receiver, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0, $def['const_args'] ?? []), 'php_fn_ref' => $this->genUniversalPhpFnRef($receiver, $def['fn'], $expr->args, $def['return_type']), + 'cpp_fn' => $this->genUniversalCppFn($receiver, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0), default => null, }; } @@ -291,18 +288,21 @@ trait UniversalMethodCall $this->context->beforeStmtLines[] = $streamVar . ' = ' . $receiver . ';'; $methodCall = match ($def['handler']) { - 'php_fn' => $this->genUniversalPhpFn($streamVar, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0), - 'direct_method' => $this->genUniversalDirectMethod($streamVar, $def['method'], $expr->args), + 'php_fn' => $this->genUniversalPhpFn($streamVar, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0, $def['const_args'] ?? []), + 'direct_method' => $this->genUniversalDirectMethod($streamVar, $def['method'], $expr->args, $def['int_cast_args'] ?? []), + 'cpp_fn' => $this->genUniversalCppFn($streamVar, $def['fn'], $expr->args, $def['receiver_pos'] ?? 0), default => null, }; $errorMsg = "Cannot call method '{$method}' on a closed or invalid stream resource"; $lambda = "[&]() -> php::Variant {\n"; - $lambda .= "if (!{$streamVar}.isResource()) {\n"; - $lambda .= "throwError(\"{$errorMsg}\");\n"; - $lambda .= "return php::null;\n"; - $lambda .= "}\n"; - $lambda .= "return {$methodCall};\n"; + $this->indentLevel++; + $lambda .= $this->getIndent() . "if (!{$streamVar}.isResource()) {\n"; + $lambda .= $this->getIndent() . "throwError(\"{$errorMsg}\");\n"; + $lambda .= $this->getIndent() . "return php::null;\n"; + $lambda .= $this->getIndent() . "}\n"; + $this->indentLevel--; + $lambda .= $this->getIndent() . "return {$methodCall};\n"; $lambda .= "}()"; $tmpVar = $this->addTmpVar(self::TYPE_VAR); @@ -352,14 +352,18 @@ trait UniversalMethodCall return '(' . $object . ' ' . $op . ' ' . $argExpr . ')'; } - protected function genUniversalDirectMethod(string $object, string $cppMethod, array $args): string + protected function genUniversalDirectMethod(string $object, string $cppMethod, array $args, array $intCastArgs = []): string { if (empty($args)) { return $object . '.' . $cppMethod . '()'; } $argExprs = []; - foreach ($args as $arg) { - $argExprs[] = $this->parseExpr($arg->value); + foreach ($args as $i => $arg) { + $expr = $this->parseExpr($arg->value); + if (in_array($i, $intCastArgs, true)) { + $expr = 'php::toInt(' . $expr . ')'; + } + $argExprs[] = $expr; } return $object . '.' . $cppMethod . '(' . implode(', ', $argExprs) . ')'; } @@ -375,7 +379,12 @@ trait UniversalMethodCall return 'php::' . $fn . '(' . $receiver . ')'; } - protected function genUniversalPhpFn(string $receiver, string $phpFunc, array $args, int $receiverPos = 0): string + /** + * @param int $receiverPos Position of the receiver in the final argument list. + * 0 (default) = receiver first. Non-zero values are 1-indexed (1 = before 1st user arg, + * 2 = before 2nd, etc.). e.g. hash(algo, data, binary) needs receiver as data → receiverPos=2. + */ + protected function genUniversalPhpFn(string $receiver, string $phpFunc, array $args, int $receiverPos = 0, array $constArgs = []): string { $argExprs = []; $userArgs = []; @@ -400,9 +409,53 @@ trait UniversalMethodCall } } + if ($constArgs) { + $maxPos = max(array_keys($constArgs)); + $totalSlots = max(count($argExprs) + count($constArgs), $maxPos + 1); + $final = []; + $regIdx = 0; + for ($i = 0; $i < $totalSlots; $i++) { + if (isset($constArgs[$i])) { + $final[] = $constArgs[$i]; + } else { + $final[] = $argExprs[$regIdx++]; + } + } + $argExprs = $final; + } + return 'php::call(' . $this->getFuncPtr($phpFunc) . ', php::ArgList{' . implode(', ', $argExprs) . '})'; } + // Same receiverPos semantics as genUniversalPhpFn. + protected function genUniversalCppFn(string $receiver, string $cppFunc, array $args, int $receiverPos = 0): string + { + $argExprs = []; + $userArgs = []; + foreach ($args as $arg) { + $userArgs[] = $this->parseExpr($arg->value); + } + + if ($receiverPos === 0) { + $argExprs = [$receiver]; + foreach ($userArgs as $ua) { + $argExprs[] = $ua; + } + } else { + foreach ($userArgs as $i => $ua) { + if ($i === $receiverPos - 1) { + $argExprs[] = $receiver; + } + $argExprs[] = $ua; + } + if ($receiverPos > count($userArgs)) { + $argExprs[] = $receiver; + } + } + + return $cppFunc . '(' . implode(', ', $argExprs) . ')'; + } + protected function genUniversalPhpFnRef(string $receiver, string $phpFunc, array $args, string $returnType): string { $tmpRef = $this->addTmpVar(self::TYPE_REF); diff --git a/tests/aot/string_method/misc.phpt b/tests/aot/string_method/misc.phpt index a0e5f36d..ac2102bf 100644 --- a/tests/aot/string_method/misc.phpt +++ b/tests/aot/string_method/misc.phpt @@ -112,10 +112,6 @@ function main() $s = "O'Reilly"; var_dump($s->addSlashes()); - // toString (identity) - $s = "hello"; - var_dump($s->toString()); - // toInt / toFloat / toBool $s = "123"; var_dump($s->toInt()); @@ -182,7 +178,6 @@ string(1) "a" string(1) "c" string(11) "Hello World" string(9) "O\'Reilly" -string(5) "hello" int(123) float(3.14) bool(true)