From 6309346c9acae2438152010d2d130ac10f69e4ec Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 25 May 2026 22:30:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E9=80=9A=E7=94=A8=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=AE=9E=E7=8E=B0=EF=BC=8C=E7=BA=AF=E9=9D=99=E6=80=81?= =?UTF-8?q?=EF=BC=8C=E9=9B=B6=E6=88=90=E6=9C=AC=E6=8A=BD=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/methods/array.php | 12 + examples/ref/stream.php | 5 + .../code/universal-method-int-undefined.php | 7 + .../code/universal-method-int-wrong-args.php | 7 + .../code/universal-method-literal-call.php | 6 + .../code/universal-method-no-native-types.php | 6 + phpunit/src/UniversalMethodCallTest.php | 19 + src/Php/CompilerBase.php | 161 ++++++- src/Php/UniversalMethodCall.php | 418 ++++++++++++++++++ src/polyfills.php | 5 + tests/aot/array_method/0.phpt | 24 + tests/aot/array_method/1.phpt | 18 + tests/aot/array_method/2.phpt | 29 ++ tests/aot/array_method/3.phpt | 300 +++++++++++++ tests/aot/array_method/method.phpt | 175 ++++++++ tests/aot/array_method/misc.phpt | 93 ++++ tests/aot/stream_method/0.phpt | 35 ++ tests/aot/stream_method/1.phpt | 60 +++ tests/aot/string_method/0.phpt | 22 + tests/aot/string_method/1.phpt | 18 + tests/aot/string_method/json.phpt | 17 + tests/aot/string_method/marshal.phpt | 20 + tests/aot/string_method/match.phpt | 25 ++ tests/aot/string_method/mbstring.phpt | 66 +++ tests/aot/string_method/method.phpt | 167 +++++++ tests/aot/string_method/misc.phpt | 191 ++++++++ tests/aot/universal_method_call.php | 66 +++ tests/aot/universal_method_call.phpt | 93 ++++ 28 files changed, 2049 insertions(+), 16 deletions(-) create mode 100644 examples/methods/array.php create mode 100644 examples/ref/stream.php create mode 100644 phpunit/code/universal-method-int-undefined.php create mode 100644 phpunit/code/universal-method-int-wrong-args.php create mode 100644 phpunit/code/universal-method-literal-call.php create mode 100644 phpunit/code/universal-method-no-native-types.php create mode 100644 phpunit/src/UniversalMethodCallTest.php create mode 100644 src/Php/UniversalMethodCall.php create mode 100644 tests/aot/array_method/0.phpt create mode 100644 tests/aot/array_method/1.phpt create mode 100644 tests/aot/array_method/2.phpt create mode 100644 tests/aot/array_method/3.phpt create mode 100644 tests/aot/array_method/method.phpt create mode 100644 tests/aot/array_method/misc.phpt create mode 100644 tests/aot/stream_method/0.phpt create mode 100644 tests/aot/stream_method/1.phpt create mode 100644 tests/aot/string_method/0.phpt create mode 100644 tests/aot/string_method/1.phpt create mode 100644 tests/aot/string_method/json.phpt create mode 100644 tests/aot/string_method/marshal.phpt create mode 100644 tests/aot/string_method/match.phpt create mode 100644 tests/aot/string_method/mbstring.phpt create mode 100644 tests/aot/string_method/method.phpt create mode 100644 tests/aot/string_method/misc.phpt create mode 100644 tests/aot/universal_method_call.php create mode 100644 tests/aot/universal_method_call.phpt diff --git a/examples/methods/array.php b/examples/methods/array.php new file mode 100644 index 00000000..c6c268ae --- /dev/null +++ b/examples/methods/array.php @@ -0,0 +1,12 @@ +push(5); + +$array->slice(1, 2)->find(3); + +$a = 100; +$a->add(99)->mul(2)->sub(100); + +$s = "hello"; +$s->upper(); + diff --git a/examples/ref/stream.php b/examples/ref/stream.php new file mode 100644 index 00000000..66725972 --- /dev/null +++ b/examples/ref/stream.php @@ -0,0 +1,5 @@ +unknownMethod(); +} diff --git a/phpunit/code/universal-method-int-wrong-args.php b/phpunit/code/universal-method-int-wrong-args.php new file mode 100644 index 00000000..8995fc26 --- /dev/null +++ b/phpunit/code/universal-method-int-wrong-args.php @@ -0,0 +1,7 @@ +add(); +} diff --git a/phpunit/code/universal-method-literal-call.php b/phpunit/code/universal-method-literal-call.php new file mode 100644 index 00000000..d638506f --- /dev/null +++ b/phpunit/code/universal-method-literal-call.php @@ -0,0 +1,6 @@ +add(99); +} diff --git a/phpunit/code/universal-method-no-native-types.php b/phpunit/code/universal-method-no-native-types.php new file mode 100644 index 00000000..32465f41 --- /dev/null +++ b/phpunit/code/universal-method-no-native-types.php @@ -0,0 +1,6 @@ +add(1); +} diff --git a/phpunit/src/UniversalMethodCallTest.php b/phpunit/src/UniversalMethodCallTest.php new file mode 100644 index 00000000..71016eb1 --- /dev/null +++ b/phpunit/src/UniversalMethodCallTest.php @@ -0,0 +1,19 @@ +exec("Cannot call method `unknownMethod()` on variable of type php::Int", 'universal-method-int-undefined.php'); + } + + public function testAddWithWrongArgCount() + { + $this->exec('Method `add()` expects exactly 1 argument(s), 0 given', 'universal-method-int-wrong-args.php'); + } + + public function testMutatingMethodOnNonVarExpr() + { + $this->exec('Cannot call mutating method `push()` on a non-variable expression', 'universal-method-mutating-expr.php'); + } +} diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 1c145a1e..054a7b08 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -59,6 +59,7 @@ class CompilerBase extends \PhpAot\Core\Translator use PropertyPromotion; use MagicMethodDetector; use StdContainerParser; + use UniversalMethodCall; use Utils; public const string TYPE_VAR = 'php::Var'; @@ -67,6 +68,17 @@ class CompilerBase extends \PhpAot\Core\Translator public const string TYPE_FLOAT = 'php::Float'; public const string TYPE_OBJECT = 'php::Object'; public const string TYPE_ARRAY = 'php::Array'; + public const string TYPE_RESOURCE = 'php::Resource'; + public const string TYPE_STREAM = 'php::Stream'; + + private const array STREAM_FUNCTIONS = [ + 'fopen', + 'tmpfile', + 'fsockopen', + 'stream_socket_client', + 'stream_socket_accept', + 'popen', + ]; public const string TYPE_STD_ARRAY = 'php::StdArray'; public const string TYPE_STD_VECTOR = 'php::StdVector'; public const string TYPE_STD_MAP = 'php::StdMap'; @@ -784,6 +796,37 @@ class CompilerBase extends \PhpAot\Core\Translator return self::TYPE_VAR; } + /** + * Resolve the type of an arbitrary expression for universal method call dispatch. + * Delegates to detectTypeOfExpr — returns null when the type cannot be determined + * at compile time (i.e. detectTypeOfExpr returns TYPE_VAR). + */ + protected function resolveExprType(Node\Expr $expr): ?string + { + $type = $this->detectTypeOfExpr($expr); + return $type !== self::TYPE_VAR ? $type : null; + } + + /** + * Resolve the ClassDef for an object expression (variable or $this). + */ + private function resolveObjectClassDef(Node\Expr $expr): ?ClassDef + { + if ($expr instanceof Expr\Variable) { + $name = $this->parseIdentifier($expr); + if ($name === 'this_' && $this->classDef) { + return $this->classDef; + } + if ($this->isTypedObject($name)) { + $className = $this->getObjectType($name); + if ($this->hasClass($className)) { + return $this->getClass($className); + } + } + } + return null; + } + protected function resetFunction(): void { $this->context = new FunctionContext(); @@ -2237,6 +2280,9 @@ class CompilerBase extends \PhpAot\Core\Translator case 'Expr_FuncCall': if ($this->isNameExpr($expr->name)) { $name = $this->parseIdentifier($expr->name); + if (in_array($name, self::STREAM_FUNCTIONS) || $name === 'stream_cast') { + return self::TYPE_STREAM; + } if ($this->hasFunction($name)) { return $this->getFunction($name)->returnType; } @@ -2244,26 +2290,76 @@ class CompilerBase extends \PhpAot\Core\Translator } break; case 'Expr_MethodCall': - if ($this->isVarExpr($expr->var) and $this->isNamedMethod($expr->name)) { - $object = $this->parseIdentifier($expr->var); + if ($this->isNamedMethod($expr->name)) { $method = $this->parseIdentifier($expr->name); - $nativeFunc = $this->findNativeMethod($expr, $object, $method); - if ($nativeFunc) { - $funcDef = $this->getFunction($nativeFunc); - return $funcDef->returnType; + // Class definition resolution (handles this_, typed VarExpr) + $classDef = $this->resolveObjectClassDef($expr->var); + if ($classDef !== null && $classDef->hasMethod($method)) { + return $classDef->getMethod($method)->getReturnType(); + } + if ($this->isVarExpr($expr->var)) { + $object = $this->parseIdentifier($expr->var); + $nativeFunc = $this->findNativeMethod($expr, $object, $method); + if ($nativeFunc) { + $funcDef = $this->getFunction($nativeFunc); + return $funcDef->returnType; + } + if ($this->isTypedObject($object)) { + return $this->detectMethodCallReturnType($this->getObjectType($object), $method); + } + $type = $this->getVarType($object); + } else { + $type = $this->detectTypeOfExpr($expr->var); + } + if ($type !== self::TYPE_VAR && !$this->checkArgType($type, self::TYPE_OBJECT)) { + $retType = $this->detectUniversalMethodReturnType($type, $method); + if ($retType !== null) { + return $retType; + } + } + } + break; + case 'Expr_StaticCall': + if ($this->isNameExpr($expr->class) && $this->isIdExpr($expr->name)) { + $className = $this->parseIdentifier($expr->class); + if ($className === 'self') { + $className = $this->getFullClassName(); + } elseif ($className === 'parent') { + if ($this->classDef->extends) { + $className = $this->classDef->extends; + } else { + break; + } + } elseif ($className === 'static') { + break; + } else { + $className = $this->getNamespacedClassName($className); } - if ($this->isTypedObject($object)) { - return $this->detectMethodCallReturnType($this->getObjectType($object), $method); + if ($this->hasClass($className)) { + $classDef = $this->getClass($className); + $methodName = $this->parseIdentifier($expr->name); + if ($classDef->hasMethod($methodName)) { + return $classDef->getMethod($methodName)->getReturnType(); + } } } break; case 'Expr_PropertyFetch': - if ($this->isVarExpr($expr->var) and $this->isIdExpr($expr->name)) { - $this->parsePropertyFetch($expr); - if ($expr->getAttribute('nativePropertyVar')) { - $propVar = $expr->getAttribute('nativePropertyVar'); - $info = $this->context->objectProps[$propVar]; - return $info['type']; + if ($this->isIdExpr($expr->name)) { + // Class definition property type + $propName = $this->parseIdentifier($expr->name); + $classDef = $this->resolveObjectClassDef($expr->var); + if ($classDef !== null && $classDef->hasProperty($propName)) { + return $classDef->getProperty($propName)->type; + } + // Native property var type + if ($this->isVarExpr($expr->var)) { + $this->parsePropertyFetch($expr); + if ($expr->getAttribute('nativePropertyVar')) { + $propVar = $expr->getAttribute('nativePropertyVar'); + $info = $this->context->objectProps[$propVar]; + return $info['type']; + } } } break; @@ -3038,6 +3134,9 @@ class CompilerBase extends \PhpAot\Core\Translator if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) { $this->fatalError($expr, 'Unsupported function: `' . $name . '`'); } + if ($name === 'stream_cast') { + return $this->parseExpr($expr->args[0]->value); + } $nativeFn = $this->findNativeFunction($name); if ($nativeFn) { $expr->setAttribute('nativeCall', $nativeFn); @@ -4799,7 +4898,15 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->isVarExpr($expr->var) and $this->isNamedMethod($expr->name)) { $type = $this->getVarType($object); if (!$this->checkArgType($type, self::TYPE_OBJECT)) { - $this->fatalError($expr, 'Cannot call method `' . $expr->name->toString() . '()` on variable of type ' . $type); + $methodName = $expr->name->toString(); + // 非对象类型可使用内置方法 + $fn = $this->findUniversalMethodAnyType($type, $methodName); + if ($fn) { + if ($type === self::TYPE_STREAM) { + return $this->genStreamNullGuard($expr, $object, $methodName, $fn); + } + return $this->parseUniversalMethodCall($expr, $object, $method, $fn); + } } $this->context->beforeStmtLines[] = '// Method Call: ' . $object . '->' . $this->parseIdentifier($expr->name) . '()'; try { @@ -4817,6 +4924,27 @@ class CompilerBase extends \PhpAot\Core\Translator } } + // 表达式返回值也可使用内置方法:fn()->method(), $obj->fn()->method(), Foo::fn()->method(), $obj->prop->method() + if (!$this->isVarExpr($expr->var) and $this->isNamedMethod($expr->name)) { + $type = $this->resolveExprType($expr->var); + if ($type !== null and !$this->checkArgType($type, self::TYPE_OBJECT)) { + $methodName = $expr->name->toString(); + $fn = $this->findUniversalMethodAnyType($type, $methodName); + if ($fn) { + // Wrap receiver in type conversion for direct_method handlers + // since the raw expression (often from php::call()) is php::Variant + $receiver = $object; + if ($fn['handler'] === 'direct_method') { + $receiver = $this->wrapUniversalReceiver($type, $object); + } + if ($type === self::TYPE_STREAM) { + return $this->genStreamNullGuard($expr, $receiver, $methodName, $fn); + } + return $this->parseUniversalMethodCall($expr, $receiver, $methodName, $fn, false); + } + } + } + if ($this->isNamedMethod($expr->name)) { $funcName = $this->parseIdentifier($expr->name); } else { @@ -4837,7 +4965,6 @@ class CompilerBase extends \PhpAot\Core\Translator return $object . '.call(' . $methodPtr . ')'; } try { - // 类名为空,这是一个 var ,类型是 any ,编译期无法获得它的类型,需要动态调用 $class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class; return $object . '.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $funcName, $class) . ')'; } catch (PlaceHolder) { @@ -5661,6 +5788,8 @@ class CompilerBase extends \PhpAot\Core\Translator } else { $code .= $info['decl'] . ' ' . $name . '{};'; } + } elseif ($type === self::TYPE_STREAM) { + $code .= self::TYPE_VAR . ' ' . $name . ';'; } else { $code .= $type . ' ' . $name; if ($type === self::TYPE_INT or $type === self::TYPE_FLOAT or $type === self::TYPE_BOOL) { diff --git a/src/Php/UniversalMethodCall.php b/src/Php/UniversalMethodCall.php new file mode 100644 index 00000000..82c86b0d --- /dev/null +++ b/src/Php/UniversalMethodCall.php @@ -0,0 +1,418 @@ + [ + 'add' => ['handler' => 'calc_op', 'op' => '+', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 1], + 'sub' => ['handler' => 'calc_op', 'op' => '-', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 1], + 'mul' => ['handler' => 'calc_op', 'op' => '*', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 1], + 'div' => ['handler' => 'calc_op', 'op' => '/', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 1], + 'mod' => ['handler' => 'calc_op', 'op' => '%', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 1], + 'inc' => ['handler' => 'calc_inc', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], + 'dec' => ['handler' => 'calc_dec', '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], + 'toString' => ['handler' => 'convert_fn', 'fn' => 'toString', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'toBool' => ['handler' => 'convert_fn', 'fn' => 'toBool', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + ], + self::TYPE_FLOAT => [ + 'add' => ['handler' => 'calc_op', 'op' => '+', 'return_type' => self::TYPE_FLOAT, 'min_args' => 1, 'max_args' => 1], + 'sub' => ['handler' => 'calc_op', 'op' => '-', 'return_type' => self::TYPE_FLOAT, 'min_args' => 1, 'max_args' => 1], + 'mul' => ['handler' => 'calc_op', 'op' => '*', 'return_type' => self::TYPE_FLOAT, 'min_args' => 1, 'max_args' => 1], + 'div' => ['handler' => 'calc_op', 'op' => '/', 'return_type' => self::TYPE_FLOAT, 'min_args' => 1, 'max_args' => 1], + 'inc' => ['handler' => 'calc_inc', 'return_type' => self::TYPE_FLOAT, 'min_args' => 0, 'max_args' => 0], + 'dec' => ['handler' => 'calc_dec', 'return_type' => self::TYPE_FLOAT, 'min_args' => 0, 'max_args' => 0], + 'toInt' => ['handler' => 'convert_fn', 'fn' => 'toInt', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], + 'toString' => ['handler' => 'convert_fn', 'fn' => 'toString', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'toBool' => ['handler' => 'convert_fn', 'fn' => 'toBool', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + ], + self::TYPE_BOOL => [ + 'toInt' => ['handler' => 'convert_fn', 'fn' => 'toInt', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], + 'toString' => ['handler' => 'convert_fn', 'fn' => 'toString', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + ], + 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], + '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], + 'upperFirst' => ['handler' => 'php_fn', 'fn' => 'ucfirst', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'upperWords' => ['handler' => 'php_fn', 'fn' => 'ucwords', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + 'addCSlashes' => ['handler' => 'php_fn', 'fn' => 'addcslashes', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 1], + 'addSlashes' => ['handler' => 'php_fn', 'fn' => 'addslashes', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'chunkSplit' => ['handler' => 'php_fn', 'fn' => 'chunk_split', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 2], + 'countChars' => ['handler' => 'php_fn', 'fn' => 'count_chars', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 1], + 'htmlEntityDecode' => ['handler' => 'php_fn', 'fn' => 'html_entity_decode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 2], + 'htmlEntityEncode' => ['handler' => 'php_fn', 'fn' => 'htmlentities', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 3], + 'htmlSpecialCharsEncode' => ['handler' => 'php_fn', 'fn' => 'htmlspecialchars', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 3], + 'htmlSpecialCharsDecode' => ['handler' => 'php_fn', 'fn' => 'htmlspecialchars_decode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + '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], + '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], + 'decr' => ['handler' => 'php_fn', 'fn' => 'str_decrement', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'pad' => ['handler' => 'php_fn', 'fn' => 'str_pad', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 3], + 'repeat' => ['handler' => 'php_fn', 'fn' => 'str_repeat', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 1], + 'replace' => ['handler' => 'php_fn', 'fn' => 'str_replace', 'receiver_pos' => 3, 'return_type' => self::TYPE_STR, 'min_args' => 2, 'max_args' => 3], + 'iReplace' => ['handler' => 'php_fn', 'fn' => 'str_ireplace', 'receiver_pos' => 3, 'return_type' => self::TYPE_STR, 'min_args' => 2, 'max_args' => 3], + 'shuffle' => ['handler' => 'php_fn', 'fn' => 'str_shuffle', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'split' => ['handler' => 'php_fn', 'fn' => 'explode', 'receiver_pos' => 2, 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 2], + 'startsWith' => ['handler' => 'php_fn', 'fn' => 'str_starts_with', 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 1], + 'endsWith' => ['handler' => 'php_fn', 'fn' => 'str_ends_with', 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 1], + 'wordCount' => ['handler' => 'php_fn', 'fn' => 'str_word_count', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 2], + 'iCompare' => ['handler' => 'php_fn', 'fn' => 'strcasecmp', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 1], + 'compare' => ['handler' => 'php_fn', 'fn' => 'strcmp', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 1], + 'find' => ['handler' => 'php_fn', 'fn' => 'strstr', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'iFind' => ['handler' => 'php_fn', 'fn' => 'stristr', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'stripTags' => ['handler' => 'php_fn', 'fn' => 'strip_tags', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 2], + 'stripCSlashes' => ['handler' => 'php_fn', 'fn' => 'stripcslashes', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'stripSlashes' => ['handler' => 'php_fn', 'fn' => 'stripslashes', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'iIndexOf' => ['handler' => 'php_fn', 'fn' => 'stripos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'indexOf' => ['handler' => 'php_fn', 'fn' => 'strpos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'lastIndexOf' => ['handler' => 'php_fn', 'fn' => 'strrpos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'iLastIndexOf' => ['handler' => 'php_fn', 'fn' => 'strripos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'lastCharIndexOf' => ['handler' => 'php_fn', 'fn' => 'strrchr', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 1], + 'substr' => ['handler' => 'php_fn', 'fn' => 'substr', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 2], + 'substrCompare' => ['handler' => 'php_fn', 'fn' => 'substr_compare', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 4], + 'substrCount' => ['handler' => 'php_fn', 'fn' => 'substr_count', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 3], + 'substrReplace' => ['handler' => 'php_fn', 'fn' => 'substr_replace', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 3], + 'reverse' => ['handler' => 'php_fn', 'fn' => 'strrev', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + '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], + '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], + '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], + 'mbLowerFirst' => ['handler' => 'php_fn', 'fn' => 'mb_lcfirst', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + 'mbTrim' => ['handler' => 'php_fn', 'fn' => 'mb_trim', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + 'mbSubstrCount' => ['handler' => 'php_fn', 'fn' => 'mb_substr_count', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 2], + 'mbSubstr' => ['handler' => 'php_fn', 'fn' => 'mb_substr', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 3], + 'mbUpper' => ['handler' => 'php_fn', 'fn' => 'mb_strtoupper', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + 'mbLower' => ['handler' => 'php_fn', 'fn' => 'mb_strtolower', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + 'mbFind' => ['handler' => 'php_fn', 'fn' => 'mb_strstr', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbIndexOf' => ['handler' => 'php_fn', 'fn' => 'mb_strpos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbLastIndexOf' => ['handler' => 'php_fn', 'fn' => 'mb_strrpos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbILastIndexOf' => ['handler' => 'php_fn', 'fn' => 'mb_strripos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbLastCharIndexOf' => ['handler' => 'php_fn', 'fn' => 'mb_strrchr', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbILastCharIndex' => ['handler' => 'php_fn', 'fn' => 'mb_strrichr', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbLength' => ['handler' => 'php_fn', 'fn' => 'mb_strlen', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 1], + 'mbIFind' => ['handler' => 'php_fn', 'fn' => 'mb_stristr', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbIIndexOf' => ['handler' => 'php_fn', 'fn' => 'mb_stripos', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 3], + 'mbCut' => ['handler' => 'php_fn', 'fn' => 'mb_strcut', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 3], + 'mbRTrim' => ['handler' => 'php_fn', 'fn' => 'mb_rtrim', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + 'mbLTrim' => ['handler' => 'php_fn', 'fn' => 'mb_ltrim', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + 'mbDetectEncoding' => ['handler' => 'php_fn', 'fn' => 'mb_detect_encoding', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 2], + 'mbConvertEncoding' => ['handler' => 'php_fn', 'fn' => 'mb_convert_encoding', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 2], + 'mbConvertCase' => ['handler' => 'php_fn', 'fn' => 'mb_convert_case', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 2], + // 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], + // 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], + '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) --- + 'all' => ['handler' => 'php_fn', 'fn' => 'array_all', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 1], + 'any' => ['handler' => 'php_fn', 'fn' => 'array_any', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 1], + 'changeKeyCase' => ['handler' => 'php_fn', 'fn' => 'array_change_key_case', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 1], + 'chunk' => ['handler' => 'php_fn', 'fn' => 'array_chunk', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 2], + 'column' => ['handler' => 'php_fn', 'fn' => 'array_column', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 2], + 'countValues' => ['handler' => 'php_fn', 'fn' => 'array_count_values', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 0], + 'diff' => ['handler' => 'php_fn', 'fn' => 'array_diff', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], + 'diffAssoc' => ['handler' => 'php_fn', 'fn' => 'array_diff_assoc', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], + 'diffKey' => ['handler' => 'php_fn', 'fn' => 'array_diff_key', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], + 'filter' => ['handler' => 'php_fn', 'fn' => 'array_filter', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 2], + 'find' => ['handler' => 'php_fn', 'fn' => 'array_find', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 1], + 'flip' => ['handler' => 'php_fn', 'fn' => 'array_flip', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 0], + 'intersect' => ['handler' => 'php_fn', 'fn' => 'array_intersect', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], + 'intersectAssoc' => ['handler' => 'php_fn', 'fn' => 'array_intersect_assoc', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], + 'isList' => ['handler' => 'php_fn', 'fn' => 'array_is_list', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + 'keyExists' => ['handler' => 'php_fn', 'fn' => 'array_key_exists', 'receiver_pos' => 2, 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 1], + 'keyFirst' => ['handler' => 'php_fn', 'fn' => 'array_key_first', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], + 'keyLast' => ['handler' => 'php_fn', 'fn' => 'array_key_last', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], + 'keys' => ['handler' => 'php_fn', 'fn' => 'array_keys', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 2], + 'map' => ['handler' => 'php_fn', 'fn' => 'array_map', 'receiver_pos' => 2, 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], + 'pad' => ['handler' => 'php_fn', 'fn' => 'array_pad', 'return_type' => self::TYPE_ARRAY, 'min_args' => 2, 'max_args' => 2], + 'product' => ['handler' => 'php_fn', 'fn' => 'array_product', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], + 'rand' => ['handler' => 'php_fn', 'fn' => 'array_rand', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 1], + 'reduce' => ['handler' => 'php_fn', 'fn' => 'array_reduce', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'replace' => ['handler' => 'php_fn', 'fn' => 'array_replace', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => -1], + 'reverse' => ['handler' => 'php_fn', 'fn' => 'array_reverse', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 1], + 'search' => ['handler' => 'php_fn', 'fn' => 'array_search', 'receiver_pos' => 2, 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 2], + 'slice' => ['handler' => 'php_fn', 'fn' => 'array_slice', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 3], + 'sum' => ['handler' => 'php_fn', 'fn' => 'array_sum', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], + 'unique' => ['handler' => 'php_fn', 'fn' => 'array_unique', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 1], + 'values' => ['handler' => 'php_fn', 'fn' => 'array_values', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 0], + 'count' => ['handler' => 'php_fn', 'fn' => 'count', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], + '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], + 'pop' => ['handler' => 'php_fn_ref', 'fn' => 'array_pop', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], + 'push' => ['handler' => 'php_fn_ref', 'fn' => 'array_push', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => -1], + 'shift' => ['handler' => 'php_fn_ref', 'fn' => 'array_shift', 'return_type' => self::TYPE_VAR, 'min_args' => 0, 'max_args' => 0], + '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], + // 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], + 'jsonEncode' => ['handler' => 'php_fn', 'fn' => 'json_encode', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 2], + // phpx C++ methods (no PHP function equivalent) + 'set' => ['handler' => 'direct_method_mutate', 'method' => 'set', 'return_type' => self::TYPE_ARRAY, 'min_args' => 2, 'max_args' => 2], + 'get' => ['handler' => 'direct_method', 'method' => 'get', 'return_type' => self::TYPE_VAR, 'min_args' => 1, 'max_args' => 1], + 'del' => ['handler' => 'direct_method_mutate', 'method' => 'del', 'return_type' => self::TYPE_ARRAY, 'min_args' => 1, 'max_args' => 1], + 'clean' => ['handler' => 'direct_method_mutate', 'method' => 'clean', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 0], + // 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], + 'toBool' => ['handler' => 'convert_fn', 'fn' => 'toBool', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + 'toString' => ['handler' => 'convert_fn', 'fn' => 'toString', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + ], + self::TYPE_STREAM => [ + // --- stdext stream_methods --- + 'write' => ['handler' => 'php_fn', 'fn' => 'fwrite', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 2], + 'read' => ['handler' => 'php_fn', 'fn' => 'fread', 'return_type' => self::TYPE_STR, 'min_args' => 1, 'max_args' => 1], + 'close' => ['handler' => 'php_fn', 'fn' => 'fclose', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + 'dataSync' => ['handler' => 'php_fn', 'fn' => 'fdatasync', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + 'sync' => ['handler' => 'php_fn', 'fn' => 'fsync', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + 'truncate' => ['handler' => 'php_fn', 'fn' => 'ftruncate', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 1], + 'stat' => ['handler' => 'php_fn', 'fn' => 'fstat', 'return_type' => self::TYPE_ARRAY, 'min_args' => 0, 'max_args' => 0], + 'seek' => ['handler' => 'php_fn', 'fn' => 'fseek', 'return_type' => self::TYPE_INT, 'min_args' => 1, 'max_args' => 2], + 'tell' => ['handler' => 'php_fn', 'fn' => 'ftell', 'return_type' => self::TYPE_INT, 'min_args' => 0, 'max_args' => 0], + 'lock' => ['handler' => 'php_fn', 'fn' => 'flock', 'return_type' => self::TYPE_BOOL, 'min_args' => 1, 'max_args' => 2], + 'eof' => ['handler' => 'php_fn', 'fn' => 'feof', 'return_type' => self::TYPE_BOOL, 'min_args' => 0, 'max_args' => 0], + 'getChar' => ['handler' => 'php_fn', 'fn' => 'fgetc', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 0], + 'getLine' => ['handler' => 'php_fn', 'fn' => 'fgets', 'return_type' => self::TYPE_STR, 'min_args' => 0, 'max_args' => 1], + ], + ]; + + private const array MUTATING_HANDLERS = ['direct_method_mutate', 'php_fn_ref']; + + private const array TYPE_SEARCH_ORDER = [self::TYPE_STR, self::TYPE_ARRAY, self::TYPE_INT, self::TYPE_FLOAT, self::TYPE_BOOL, self::TYPE_STREAM]; + + protected function detectUniversalMethodReturnType(string $type, string $method): ?string + { + if ($type === self::TYPE_VAR) { + foreach (self::TYPE_SEARCH_ORDER as $searchType) { + $def = static::UNIVERSAL_METHODS[$searchType][$method] ?? null; + if ($def !== null) { + return $def['return_type']; + } + } + return null; + } + return static::UNIVERSAL_METHODS[$type][$method]['return_type'] ?? null; + } + + /** + * Search all type tables for a method. Returns [type, def] or null. + */ + protected function findUniversalMethodAnyType(string $type, string $method): ?array + { + if ($type === self::TYPE_VAR) { + foreach (self::TYPE_SEARCH_ORDER as $searchType) { + $def = static::UNIVERSAL_METHODS[$searchType][$method] ?? null; + if ($def !== null) { + return $def; + } + } + return null; + } + return static::UNIVERSAL_METHODS[$type][$method] ?? null; + } + + /** + * @param string $receiver C++ expression for the receiver + * @param bool $isVar Whether the receiver is a variable (allows mutating methods) + */ + protected function parseUniversalMethodCall(Node\Expr\MethodCall $expr, string $receiver, string $method, array $def, bool $isVar = true): ?string + { + $this->validateUniversalMethodArgs($expr, $method, $def, $isVar); + + return match ($def['handler']) { + '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_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_ref' => $this->genUniversalPhpFnRef($receiver, $def['fn'], $expr->args, $def['return_type']), + default => null, + }; + } + + /** + * Generate a stream method call with null guard. + * Stream resources are nullable — throws Error if the receiver is null. + */ + protected function genStreamNullGuard(Node\Expr\MethodCall $expr, string $receiver, string $method, array $def): string + { + $this->validateUniversalMethodArgs($expr, $method, $def, false); + + // Evaluate receiver into temp var to avoid double evaluation + $streamVar = $this->addTmpVar(self::TYPE_VAR); + $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), + 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"; + $lambda .= "}()"; + + $tmpVar = $this->addTmpVar(self::TYPE_VAR); + $this->context->beforeStmtLines[] = "{$tmpVar} = {$lambda};"; + return $tmpVar; + } + + /** + * Wrap a non-variable receiver expression in the appropriate type conversion + * so that direct C++ method calls (e.g. .get(), .set()) work on the correct type. + */ + private function wrapUniversalReceiver(string $type, string $expr): string + { + $convFns = [ + self::TYPE_ARRAY => 'toArray', + self::TYPE_STR => 'toString', + self::TYPE_INT => 'toInt', + self::TYPE_FLOAT => 'toFloat', + self::TYPE_BOOL => 'toBool', + ]; + if (isset($convFns[$type])) { + return 'php::' . $convFns[$type] . '(' . $expr . ')'; + } + return $expr; + } + + private function validateUniversalMethodArgs(Node\Expr\MethodCall $expr, string $method, array $def, bool $isVar): void + { + $argCount = count($expr->args); + $maxArgs = $def['max_args']; + if ($maxArgs === -1) { + $maxArgs = PHP_INT_MAX; + } + if ($argCount < $def['min_args'] || $argCount > $maxArgs) { + $expected = $def['min_args'] === $def['max_args'] ? "exactly {$def['min_args']}" : "{$def['min_args']} to {$def['max_args']}"; + $this->fatalError($expr, "Method `{$method}()` expects {$expected} argument(s), {$argCount} given"); + } + + if (!$isVar && in_array($def['handler'], self::MUTATING_HANDLERS, true)) { + $this->fatalError($expr, 'Cannot call mutating method `' . $method . '()` on a non-variable expression'); + } + } + + protected function genUniversalCalcOp(string $object, string $op, array $args): string + { + $argExpr = $this->parseExpr($args[0]->value); + return '(' . $object . ' ' . $op . ' ' . $argExpr . ')'; + } + + protected function genUniversalDirectMethod(string $object, string $cppMethod, array $args): string + { + if (empty($args)) { + return $object . '.' . $cppMethod . '()'; + } + $argExprs = []; + foreach ($args as $arg) { + $argExprs[] = $this->parseExpr($arg->value); + } + return $object . '.' . $cppMethod . '(' . implode(', ', $argExprs) . ')'; + } + + protected function genUniversalMutatingMethod(string $object, string $cppMethod, array $args): string + { + $call = $this->genUniversalDirectMethod($object, $cppMethod, $args); + return '(' . $call . ', ' . $object . ')'; + } + + protected function genUniversalConvertFn(string $receiver, string $fn, string $type): string + { + return 'php::' . $fn . '(' . $receiver . ')'; + } + + protected function genUniversalPhpFn(string $receiver, string $phpFunc, 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 'php::call(' . $this->getFuncPtr($phpFunc) . ', php::ArgList{' . implode(', ', $argExprs) . '})'; + } + + protected function genUniversalPhpFnRef(string $receiver, string $phpFunc, array $args, string $returnType): string + { + $tmpRef = $this->addTmpVar(self::TYPE_REF); + $tmpVar = $this->addTmpVar(self::TYPE_VAR); + $argExprs = ['&' . $tmpRef]; + foreach ($args as $arg) { + $argExprs[] = $this->parseExpr($arg->value); + } + $this->context->beforeStmtLines[] = $tmpRef . ' = ' . $receiver . '.toReference();'; + $this->context->beforeStmtLines[] = $tmpVar . ' = php::call(' . $this->getFuncPtr($phpFunc) . ', php::ArgList{' . implode(', ', $argExprs) . '});'; + return $tmpVar; + } +} diff --git a/src/polyfills.php b/src/polyfills.php index cb0ac106..f69234cb 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -95,3 +95,8 @@ function any(mixed $var): mixed { return $var; } + +function stream_cast($stream) +{ + return $stream; +} diff --git a/tests/aot/array_method/0.phpt b/tests/aot/array_method/0.phpt new file mode 100644 index 00000000..1e29369f --- /dev/null +++ b/tests/aot/array_method/0.phpt @@ -0,0 +1,24 @@ +--TEST-- +array_method: 0 +--FILE-- +isEmpty()); + Assert::eq($array->count(), count($array)); + Assert::eq($array->slice(0, 2), [1, 2]); + Assert::true($array->contains(999)); + Assert::eq($array->search(999), 3); + + $array2 = []; + Assert::true($array2->isEmpty()); +} +?> +--EXPECT-- diff --git a/tests/aot/array_method/1.phpt b/tests/aot/array_method/1.phpt new file mode 100644 index 00000000..d39dd1e8 --- /dev/null +++ b/tests/aot/array_method/1.phpt @@ -0,0 +1,18 @@ +--TEST-- +array_method: 1 +--FILE-- +sort(SORT_NATURAL | SORT_FLAG_CASE); + Assert::same($array, $sorted_array); + echo "done\n"; +} +?> +--EXPECT-- +done diff --git a/tests/aot/array_method/2.phpt b/tests/aot/array_method/2.phpt new file mode 100644 index 00000000..6709d14d --- /dev/null +++ b/tests/aot/array_method/2.phpt @@ -0,0 +1,29 @@ +--TEST-- +Universal method: array_method/2 (shift/unshift on Ref) +--FILE-- +count(), 4); + $fruit = $stack->shift(); + Assert::eq($fruit, "orange"); + Assert::eq($stack->count(), 3); + Assert::eq($array->count(), 4); + + $stack->unshift("mango"); + Assert::eq($stack->count(), 4); + + $stack2 = array("orange", "banana", "apple", "raspberry"); + Assert::eq($stack2->count(), 4); + $stack2->shift(); + Assert::eq($stack2->count(), 3); + + echo "done\n"; +} +?> +--EXPECT-- +done diff --git a/tests/aot/array_method/3.phpt b/tests/aot/array_method/3.phpt new file mode 100644 index 00000000..10b13c86 --- /dev/null +++ b/tests/aot/array_method/3.phpt @@ -0,0 +1,300 @@ +--TEST-- +Universal method call: Array type methods +--FILE-- +isEmpty()); + $arr->push(10); + $arr->push(20); + $arr->push(30); + var_dump($arr->count()); + var_dump($arr->isEmpty()); + + // pop + $arr = []; + $arr->push(100); + $arr->push(200); + $arr->push(300); + var_dump($arr->pop()); + var_dump($arr->count()); + var_dump($arr->pop()); + var_dump($arr->pop()); + + // get / set / del / keyExists + $arr = []; + $arr->set("name", "John"); + $arr->set("age", 30); + var_dump($arr->get("name")); + var_dump($arr->get("age")); + var_dump($arr->keyExists("age")); + $arr->del("age"); + var_dump($arr->keyExists("age")); + var_dump($arr->keyExists("name")); + + // clean + $arr = []; + $arr->push(1); + $arr->push(2); + $arr->push(3); + $arr->clean(); + var_dump($arr->isEmpty()); + var_dump($arr->count()); + + // keyExists / contains + $arr = []; + $arr->push("apple"); + $arr->push("banana"); + $arr->push("cherry"); + var_dump($arr->keyExists(0)); + var_dump($arr->keyExists(5)); + var_dump($arr->contains("banana")); + var_dump($arr->contains("grape")); + + // search + $arr = []; + $arr->push("red"); + $arr->push("green"); + $arr->push("blue"); + $arr->push("green"); + var_dump($arr->search("green")); + var_dump($arr->search("yellow")); + + // isList + $arr = []; + $arr->push("a"); + $arr->push("b"); + $arr->push("c"); + var_dump($arr->isList()); + $hash = []; + $hash->set("k", "v"); + var_dump($hash->isList()); + + // keys / values + $arr = []; + $arr->set("x", 1); + $arr->set("y", 2); + $arr->set("z", 3); + $keys = $arr->keys(); + var_dump($keys->count()); + var_dump($keys->contains("x")); + var_dump($keys->contains("y")); + $vals = $arr->values(); + var_dump($vals->contains(1)); + var_dump($vals->contains(3)); + + // keyFirst / keyLast + var_dump($arr->keyFirst()); + var_dump($arr->keyLast()); + + // join + $arr = []; + $arr->push("one"); + $arr->push("two"); + $arr->push("three"); + var_dump($arr->join(", ")); + + // merge (mutating) + $arr1 = []; + $arr1->push("a"); + $arr1->push("b"); + $arr2 = []; + $arr2->push("c"); + $arr2->push("d"); + $arr1->merge($arr2); + var_dump($arr1->count()); + var_dump($arr1->get(2)); + + // slice + $arr = []; + $arr->push("a"); + $arr->push("b"); + $arr->push("c"); + $arr->push("d"); + $arr->push("e"); + $sliced = $arr->slice(1, 3); + var_dump($sliced->count()); + var_dump($sliced->get(0)); + $sliced2 = $arr->slice(-2); + var_dump($sliced2->count()); + var_dump($sliced2->get(0)); + + // sort (mutating) + $arr = []; + $arr->push(3); + $arr->push(1); + $arr->push(4); + $arr->push(1); + $arr->push(5); + $arr->sort(); + var_dump($arr->get(0)); + var_dump($arr->get(1)); + var_dump($arr->get(4)); + + // reverse + $arr = []; + $arr->push(1); + $arr->push(2); + $arr->push(3); + $reversed = $arr->reverse(); + var_dump($reversed->get(0)); + var_dump($reversed->get(2)); + + // unique + $arr = []; + $arr->push("a"); + $arr->push("b"); + $arr->push("a"); + $arr->push("c"); + $unique = $arr->unique(); + var_dump($unique->count()); + + // flip + $arr = []; + $arr->set("a", 1); + $arr->set("b", 2); + $flipped = $arr->flip(); + var_dump($flipped->keyExists(1)); + + // sum / product + $arr = []; + $arr->push(2); + $arr->push(4); + $arr->push(6); + var_dump($arr->sum()); + var_dump($arr->product()); + + // chunk + $arr = []; + $arr->push(1); + $arr->push(2); + $arr->push(3); + $arr->push(4); + $arr->push(5); + $chunks = $arr->chunk(2); + var_dump($chunks->count()); + + // diff + $a = []; + $a->push("green"); + $a->push("red"); + $a->push("blue"); + $b = []; + $b->push("green"); + $b->push("yellow"); + $diff = $a->diff($b); + var_dump($diff->count()); + var_dump($diff->contains("red")); + var_dump($diff->contains("blue")); + + // filter + $arr = []; + $arr->push(1); + $arr->push(2); + $arr->push(3); + $arr->push(4); + $evens = $arr->filter('_even_filter'); + var_dump($evens->count()); + var_dump($evens->get(1)); + + // values + $arr = []; + $arr->set("x", 10); + $arr->set("y", 20); + $vals2 = $arr->values(); + var_dump($vals2->count()); + var_dump($vals2->get(0)); + var_dump($vals2->get(1)); + + // replace + $base = []; + $base->push("orange"); + $base->push("banana"); + $base->push("apple"); + $repl = []; + $repl->push("pineapple"); + $result = $base->replace($repl); + var_dump($result->get(0)); + + // jsonEncode + $arr = []; + $arr->set("name", "test"); + $arr->set("value", 123); + var_dump($arr->jsonEncode()); + + // toInt / toFloat / toBool / toString + $arr = []; + $arr->push(1); + var_dump($arr->toInt()); + var_dump($arr->toFloat()); + var_dump($arr->toBool()); + + echo "done\n"; +} +?> +--EXPECT-- +bool(true) +int(3) +bool(false) +int(300) +int(2) +int(200) +int(100) +string(4) "John" +int(30) +bool(true) +bool(false) +bool(true) +bool(true) +int(0) +bool(true) +bool(false) +bool(true) +bool(false) +int(1) +bool(false) +bool(true) +bool(false) +int(3) +bool(true) +bool(true) +bool(true) +bool(true) +string(1) "x" +string(1) "z" +string(15) "one, two, three" +int(4) +string(1) "c" +int(3) +string(1) "b" +int(1) +string(1) "d" +int(1) +int(1) +int(5) +int(3) +int(1) +int(3) +bool(true) +int(12) +int(48) +int(3) +int(2) +bool(true) +bool(true) +int(2) +int(2) +int(2) +int(10) +int(20) +string(9) "pineapple" +string(27) "{"name":"test","value":123}" +int(1) +float(1) +bool(true) +done diff --git a/tests/aot/array_method/method.phpt b/tests/aot/array_method/method.phpt new file mode 100644 index 00000000..2834287f --- /dev/null +++ b/tests/aot/array_method/method.phpt @@ -0,0 +1,175 @@ +--TEST-- +array_method: all array methods test +--FILE-- + 1, "SecOnd" => 4); + Assert::eq($array->changeKeyCase(CASE_UPPER), array_change_key_case($array, CASE_UPPER)); + + $array = array('a', 'b', 'c', 'd', 'e'); + Assert::eq($array->chunk(2), array_chunk($array, 2)); + + $records = [ + [ + 'id' => 2135, + 'first_name' => 'John', + 'last_name' => 'Doe', + ], + [ + 'id' => 3245, + 'first_name' => 'Sally', + 'last_name' => 'Smith', + ], + [ + 'id' => 5342, + 'first_name' => 'Jane', + 'last_name' => 'Jones', + ], + [ + 'id' => 5623, + 'first_name' => 'Peter', + 'last_name' => 'Doe', + ] + ]; + Assert::eq($records->column('id'), array_column($records, 'id')); + + $array = array(1, "hello", 1, "world", "hello"); + Assert::eq($array->countValues(), array_count_values($array)); + + $array1 = array("a" => "green", "red", "blue", "red"); + $array2 = array("b" => "green", "yellow", "red"); + Assert::eq($array1->diff($array2), array_diff($array1, $array2)); + + $array1 = array("a" => "green", "b" => "brown", "c" => "blue", "red"); + $array2 = array("a" => "green", "yellow", "red"); + Assert::eq($array1->diffAssoc($array2), array_diff_assoc($array1, $array2)); + + $array1 = array('blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4); + $array2 = array('green' => 5, 'yellow' => 7, 'cyan' => 8); + Assert::eq($array1->diffKey($array2), array_diff_key($array1, $array2)); + + $array = [6, 7, 8, 9, 10, 11, 12]; + Assert::eq($array->filter('_odd'), array_filter($array, '_odd')); + + $input = array("oranges", "apples", "pears"); + Assert::eq($input->flip(), array_flip($input)); + + $array1 = array("a" => "green", "red", "blue"); + $array2 = array("b" => "green", "yellow", "red"); + Assert::eq($array1->intersect($array2), array_intersect($array1, $array2)); + Assert::eq($array1->intersectAssoc($array2), array_intersect_assoc($array1, $array2)); + + $array = ['apple', 2, 3]; + Assert::eq($array->isList(), array_is_list($array)); + + $array = ['first' => 1, 'second' => 4]; + Assert::eq($array->keyExists('first'), array_key_exists('first', $array)); + + $array = ['a' => 1, 'b' => 2, 'c' => 3]; + Assert::eq($array->keyFirst(), array_key_first($array)); + Assert::eq($array->keyLast(), array_key_last($array)); + Assert::eq($array->keys(), array_keys($array)); + Assert::eq($array->values(), array_values($array)); + + $a = [1, 2, 3, 4, 5]; + Assert::eq($a->map('_cube'), array_map('_cube', $a)); + + $array = array(12, 10, 9); + Assert::eq($array->pad(5, 0), array_pad($array, 5, 0)); + + $a = array(2, 4, 6, 8); + Assert::eq($a->product(), array_product($a)); + + $array = array("Neo", "Morpheus", "Trinity", "Cypher", "Tank"); + Assert::notEq($array->rand(2), array_rand($array, 2)); + + $array = array(1, 2, 3, 4, 5); + Assert::eq($array->reduce('_sum'), array_reduce($array, '_sum')); + + $base = array("orange", "banana", "apple", "raspberry"); + $replacements = array(0 => "pineapple", 4 => "cherry"); + $replacements2 = array(0 => "grape"); + Assert::eq($base->replace($replacements, $replacements2), array_replace($base, $replacements, $replacements2)); + + $array = array("php", 4.0, array("green", "red")); + $rev = $array->reverse(); + Assert::eq($rev->reverse(), array_reverse(array_reverse($array))); + + $array = array(0 => 'blue', 1 => 'red', 2 => 'green', 3 => 'red'); + Assert::eq($array->search('green'), array_search('green', $array)); + + $array = array("a", "b", "c", "d", "e"); + Assert::eq($array->slice(-2, 1), array_slice($array, -2, 1)); + + $a = array(2, 4, 6, 8); + Assert::eq($a->sum(), array_sum($a)); + + $array = ["a" => "green", "red", "b" => "green", "blue", "red"]; + Assert::eq($array->unique(), array_unique($array)); + Assert::eq($array->count(), count($array)); + + $array = array("Mac", "NT", "Irix", "Linux"); + Assert::eq($array->contains("Mac"), in_array("Mac", $array)); + Assert::eq($array->join(","), implode(",", $array)); + + $array = []; + Assert::true($array->isEmpty()); + + $fruits1 = array("lemon", "orange", "banana", "apple"); + $fruits2 = array("lemon", "orange", "banana", "apple"); + sort($fruits2); + Assert::eq($fruits1->sort(), $fruits2); + Assert::eq($fruits1, $fruits2); + + $stack = array("orange", "banana", "apple", "raspberry"); + Assert::eq($stack->pop(), 'raspberry'); + Assert::eq(array_pop($stack), 'apple'); + + $array = array("red","green"); + $array->push("blue"); + Assert::eq($array, ["red","green", "blue"]); + array_push($array, "yellow"); + Assert::eq($array, ["red","green", "blue", "yellow"]); + + $stack = array("orange", "banana", "apple", "raspberry"); + Assert::eq($stack->shift(), 'orange'); + Assert::eq(array_shift($stack), 'banana'); + + $queue = ["orange", "banana"]; + $queue->unshift("orange"); + Assert::eq($queue, ["orange", "orange", "banana"]); + array_unshift($queue, "orange"); + Assert::eq($queue, ["orange", "orange", "orange", "banana"]); + + $array1 = array("red", "green", "blue", "yellow"); + $array2 = array("red", "green", "blue", "yellow"); + Assert::eq($array1->splice(2), array_splice($array2, 2)); + Assert::eq($array1, $array2); + + $find = array("Hello","world"); + $replace = array("B"); + $arr = array("Hello","world","!"); + Assert::eq($arr->replaceStr($find, $replace), str_replace($find, $replace, $arr)); +} +?> +--EXPECT-- diff --git a/tests/aot/array_method/misc.phpt b/tests/aot/array_method/misc.phpt new file mode 100644 index 00000000..00d4d0ea --- /dev/null +++ b/tests/aot/array_method/misc.phpt @@ -0,0 +1,93 @@ +--TEST-- +Universal method call on native typed variables +--FILE-- +add(50); + var_dump($a); + + $a->sub(30); + var_dump($a); + + $a->mul(2); + var_dump($a); + + $a->div(4); + var_dump($a); + + $b = $a->add(10); + var_dump($b); + var_dump($a); + + // Int conversion + var_dump($a->toString()); + var_dump($a->toFloat()); + + // String methods + $s = "hello world"; + var_dump($s->length()); + var_dump($s->upper()); + var_dump($s->substr(0, 5)); + $words = $s->split(" "); + var_dump($words->count()); + + // Array methods + $arr = []; + $arr->push(100); + $arr->push(200); + $arr->push(300); + var_dump($arr->count()); + var_dump($arr->get(0)); + var_dump($arr->get(1)); + var_dump($arr->keyExists(0)); + var_dump($arr->isEmpty()); + var_dump($arr->isList()); + + // Array pop + $last = $arr->pop(); + var_dump($last); + var_dump($arr->count()); + + // Array clean + $arr->clean(); + var_dump($arr->isEmpty()); + + // Float methods + $f = 3.14; + $f->add(1.0); + var_dump($f); + var_dump($f->toInt()); + + echo "done\n"; +} +?> +--EXPECT-- +int(100) +int(100) +int(100) +int(100) +int(110) +int(100) +string(3) "100" +float(100) +int(11) +string(11) "HELLO WORLD" +string(5) "hello" +int(2) +int(3) +int(100) +int(200) +bool(true) +bool(false) +bool(true) +int(300) +int(2) +bool(true) +float(3.14) +int(3) +done diff --git a/tests/aot/stream_method/0.phpt b/tests/aot/stream_method/0.phpt new file mode 100644 index 00000000..974bea47 --- /dev/null +++ b/tests/aot/stream_method/0.phpt @@ -0,0 +1,35 @@ +--TEST-- +stream_method: 0 +--SKIPIF-- +--FILE-- +write($rdata->base64Encode()), $rdata->length()); +$fp->seek(0); +Assert::eq($rdata, $fp->read(8192)->base64Decode()); +Assert::eq($fp->stat(), fstat($fp)); +Assert::true($fp->sync()); +Assert::true($fp->dataSync()); +$fp->seek(100); +Assert::true($fp->tell() == 100); +Assert::true($fp->lock(LOCK_SH) == true); +Assert::true($fp->lock(LOCK_UN) == true); +Assert::true($fp->eof() == feof($fp)); + +$fp->seek(0); +$char = $fp->getChar(); +$fp->seek(0); +Assert::eq($char, fgetc($fp)); +$fp->seek(0); +$line = $fp->getLine(); +$fp->seek(0); +Assert::eq($line, fgets($fp)); +Assert::true($fp->truncate(1000)); + +Assert::true($fp->close()); +?> +--EXPECT-- diff --git a/tests/aot/stream_method/1.phpt b/tests/aot/stream_method/1.phpt new file mode 100644 index 00000000..16054581 --- /dev/null +++ b/tests/aot/stream_method/1.phpt @@ -0,0 +1,60 @@ +--TEST-- +Universal method call on stream resources with null guard +--FILE-- +write() returns bytes written (php_fn: fwrite) + $tmpfile = tempnam(sys_get_temp_dir(), 'aot'); + var_dump(fopen($tmpfile, 'w')->write("hello") !== false); + + // Test 2: fopen()->read() returns content (php_fn: fread) + // (re-open for reading — new stream) + var_dump(fopen($tmpfile, 'r')->read(5)); + + // Test 3: fopen()->close() returns bool (php_fn: fclose) + var_dump(fopen($tmpfile, 'r')->close()); + + // Test 4: fopen()->eof() returns bool (php_fn: feof) + $f = fopen($tmpfile, 'w'); + $f->write("test"); + $f->close(); + var_dump(fopen($tmpfile, 'r')->eof()); + + // Test 5: fopen()->tell() returns position (php_fn: ftell) + $r = fopen($tmpfile, 'r'); + var_dump($r->tell() === 0); + $r->read(2); + var_dump($r->tell() === 2); + $r->close(); + + // Test 6: fopen()->getChar() returns single char (php_fn: fgetc) + $f2 = fopen($tmpfile, 'r'); + var_dump($f2->getChar()); + $f2->close(); + + // Test 7: Null guard — fopen() with invalid mode causes null, + // calling write() should throw Error + try { + @fopen($tmpfile, 'INVALID_MODE')->write('data'); + var_dump(false); // should not reach here + } catch (\Error $e) { + var_dump(str_contains($e->getMessage(), 'closed or invalid stream resource')); + } + + unlink($tmpfile); + echo "OK\n"; +} +?> +--EXPECT-- +bool(true) +string(5) "hello" +bool(true) +bool(false) +bool(true) +bool(true) +string(1) "t" +bool(true) +OK + diff --git a/tests/aot/string_method/0.phpt b/tests/aot/string_method/0.phpt new file mode 100644 index 00000000..a1b45a59 --- /dev/null +++ b/tests/aot/string_method/0.phpt @@ -0,0 +1,22 @@ +--TEST-- +string_method: 0 +--FILE-- +isEmpty()); + Assert::eq($string->length(), strlen($string)); + Assert::eq($string->substr(0, 5), 'hello'); + Assert::eq($string->contains('world'), true); + Assert::eq($string->indexOf('test'), strpos($string, 'test')); + Assert::eq($string->split(' '), explode(' ', $string)); + + $empty = ''; + Assert::true($empty->isEmpty()); +} +?> +--EXPECT-- diff --git a/tests/aot/string_method/1.phpt b/tests/aot/string_method/1.phpt new file mode 100644 index 00000000..8e21ea1e --- /dev/null +++ b/tests/aot/string_method/1.phpt @@ -0,0 +1,18 @@ +--TEST-- +string_method: 1 +--FILE-- +parseStr(); + Assert::eq($output['first'], 'value'); + Assert::eq($output['arr'][0], 'foo bar'); + Assert::eq($output['arr'][1], 'baz'); +} +?> +--EXPECT-- diff --git a/tests/aot/string_method/json.phpt b/tests/aot/string_method/json.phpt new file mode 100644 index 00000000..afca04f1 --- /dev/null +++ b/tests/aot/string_method/json.phpt @@ -0,0 +1,17 @@ +--TEST-- +string_method: json +--FILE-- + random_bytes(128)->base64Encode(), 'b' => random_int(1, PHP_INT_MAX), 'c' => php_uname()]; + + $str = $array->jsonEncode(); + Assert::notEmpty($str); + Assert::eq($str->jsonDecode(), $array); +} +?> +--EXPECT-- diff --git a/tests/aot/string_method/marshal.phpt b/tests/aot/string_method/marshal.phpt new file mode 100644 index 00000000..20d236c4 --- /dev/null +++ b/tests/aot/string_method/marshal.phpt @@ -0,0 +1,20 @@ +--TEST-- +string_method: marshal +--FILE-- +set('a', 'marshal_test_value'); + $array->set('b', 100); + $array->set('c', 'system_info'); + + $str = $array->marshal(); + Assert::notEmpty($str); + Assert::eq($str->unmarshal(), $array); +} +?> +--EXPECT-- diff --git a/tests/aot/string_method/match.phpt b/tests/aot/string_method/match.phpt new file mode 100644 index 00000000..5faeb5a0 --- /dev/null +++ b/tests/aot/string_method/match.phpt @@ -0,0 +1,25 @@ +--TEST-- +string_method: match +--FILE-- +match($regex1, PREG_OFFSET_CAPTURE); + + preg_match($regex1, $str, $matches2, PREG_OFFSET_CAPTURE); + Assert::eq($matches, $matches2); + + $html = "bold textclick me"; + $regex2 = "/(<([\w]+)[^>]*>)(.*?)(<\/\\2>)/"; + preg_match_all($regex2, $html, $matches2, PREG_SET_ORDER); + + $matches = $html->matchAll($regex2, PREG_SET_ORDER); + Assert::eq($matches, $matches2); +} +?> +--EXPECT-- diff --git a/tests/aot/string_method/mbstring.phpt b/tests/aot/string_method/mbstring.phpt new file mode 100644 index 00000000..86395eb6 --- /dev/null +++ b/tests/aot/string_method/mbstring.phpt @@ -0,0 +1,66 @@ +--TEST-- +string_method: mbstring +--SKIPIF-- + +--FILE-- +mbUpperFirst(), mb_ucfirst($text)); + + $text = 'Bbbb大声向宇宙呐喊bbbb'; + Assert::eq($text->mbLowerFirst(), mb_lcfirst($text)); + + $text = "\t\t大声向宇宙呐喊 :) ... "; + Assert::eq($text->mbTrim(), mb_trim($text)); + Assert::eq($text->mbLTrim(), mb_ltrim($text)); + Assert::eq($text->mbRTrim(), mb_rtrim($text)); + + $text = '大声大声大声向宇宙呐喊'; + Assert::eq($text->mbSubstrCount('大声'), mb_substr_count($text, '大声')); + + $text = "大声大声大声向宇宙呐喊"; + Assert::eq($text->mbSubstr(0, -1), mb_substr($text, 0, -1)); + + $text = 'b大声大声大声向宇宙呐喊'; + Assert::eq($text->mbUpper(), mb_strtoupper($text)); + + $text = 'BBBBBB大声大声大声向宇宙呐喊'; + Assert::eq($text->mbLower(), mb_strtolower($text)); + + $email = 'name我哦知道@example.com'; + Assert::eq($email->mbFind('哦'), mb_strstr($email, '哦')); + Assert::eq($email->mbIFind('道'), mb_stristr($email, '道')); + + $mystring1 = '当地特产'; + $mystring2 = '现在只是谈谈'; + Assert::true($mystring1->mbIndexOf('地') === mb_strpos($mystring1, '地')); + Assert::true($mystring2->mbIIndexOf('A') === mb_stripos($mystring2, 'a')); + + $mystring = '啦啦啦啦啦啦啦'; + Assert::true($mystring->mbLastIndexOf('好') === false); + Assert::true(mb_strrpos($mystring, '好') === false); + Assert::eq($mystring->mbILastIndexOf('啦'), mb_strripos($mystring, '啦')); + Assert::eq($mystring->mbLastCharIndexOf('啦'), mb_strrchr($mystring, '啦')); + Assert::eq($mystring->mbILastCharIndex('啦'), mb_strrichr($mystring, '啦')); + + $text = '啦啦啦啦啦啦啦'; + Assert::eq($text->mbLength(), mb_strlen($text)); + Assert::eq($text->mbCut(0, 2), mb_strcut($text, 0, 2)); + Assert::eq($text->mbDetectEncoding(), mb_detect_encoding($text)); + Assert::eq($text->mbConvertEncoding('GBK'), mb_convert_encoding($text, 'GBK')); + + $text = 'bbbbba啦啦啦啦啦啦啦'; + Assert::eq( + $text->mbConvertCase(MB_CASE_UPPER)->mbConvertCase(MB_CASE_LOWER), + mb_convert_case(mb_convert_case($text, MB_CASE_UPPER), MB_CASE_LOWER) + ); +} +?> +--EXPECT-- diff --git a/tests/aot/string_method/method.phpt b/tests/aot/string_method/method.phpt new file mode 100644 index 00000000..bcd73768 --- /dev/null +++ b/tests/aot/string_method/method.phpt @@ -0,0 +1,167 @@ +--TEST-- +string_method: all string methods test +--FILE-- +length(), strlen($text)); + + $text = ''; + Assert::eq($text->isEmpty(), true); + + $text = 'A'; + Assert::eq($text->lower(), strtolower($text)); + Assert::eq($text->lowerFirst(), lcfirst($text)); + + $text = 'b'; + Assert::eq($text->upper(), strtoupper($text)); + Assert::eq($text->upperFirst(), ucfirst($text)); + + $text = 'hello world!'; + Assert::eq($text->upperWords(), ucwords($text)); + + $text = "PHP isThirty\nYears Old!\tYay to the Elephant!\n"; + $characters = "\0..\37!@\177..\377"; + Assert::eq($text->addCSlashes($characters), addCSlashes($text, $characters)); + + $text = "O'Reilly?"; + Assert::eq($text->addSlashes(), addslashes($text)); + + $text = 'This is quite a long string, which will get broken up because the line is going to be too long after base64 encoding it.'; + $encoded = $text->base64Encode(); + Assert::eq($encoded->chunkSplit(), chunk_split(base64_encode($text))); + + $text = "Two Ts and one F."; + Assert::eq($text->countChars(1), count_chars($text, 1)); + + $text = "I'll \"walk\" the dog now"; + $encoded = $text->htmlEntityEncode(); + Assert::eq($encoded->htmlEntityDecode(), html_entity_decode(htmlentities($text))); + + $text = "Test"; + $encoded = $text->htmlSpecialCharsEncode(ENT_QUOTES); + Assert::eq($encoded->htmlSpecialCharsDecode(), htmlspecialchars_decode(htmlspecialchars($text, ENT_QUOTES))); + + $text = "\t\tThese are a few words :) ... "; + Assert::eq($text->trim(), trim($text)); + Assert::eq($text->lTrim(), ltrim($text)); + Assert::eq($text->rTrim(), rtrim($text)); + + $text = "first=value&arr[]=foo+bar&arr[]=baz"; + parse_str($text, $result); + Assert::eq($text->parseStr(), $result); + + $url = 'http://username:password@hostname:9090/path?arg=value#anchor'; + Assert::eq($url->parseUrl(), parse_url($url)); + + $text = 'The lazy fox jumped over the fence'; + Assert::eq($text->contains('lazy'), str_contains($text, 'lazy')); + + $text = 'ABC'; + Assert::eq($text->incr(), str_increment($text)); + Assert::eq($text->decr(), str_decrement($text)); + + $text = ""; + Assert::eq($text->replace("%BODY%", "black"), str_replace("%BODY%", "black", $text)); + Assert::eq($text->iReplace("%body%", "black"), str_ireplace("%body%", "black", $text)); + + $text = "Alien"; + Assert::eq($text->pad(10), str_pad($text, 10)); + + $text = "-="; + Assert::eq($text->repeat(10), str_repeat($text, 10)); + + $text = 'abcdef'; + Assert::notEq($text->shuffle(), str_shuffle($text)); + + $text = "piece1 piece2 piece3 piece4 piece5 piece6"; + Assert::eq($text->split(' '), explode(' ', $text)); + + $text = 'The lazy fox jumped over the fence'; + Assert::eq($text->startsWith('The'), str_starts_with($text, 'The')); + Assert::eq($text->endsWith('fence'), str_ends_with($text, 'fence')); + + $text = "Hello fri3nd, you're\n looking good today!"; + Assert::eq($text->wordCount(0), str_word_count($text, 0)); + Assert::eq($text->wordCount(1), str_word_count($text, 1)); + Assert::eq($text->wordCount(2), str_word_count($text, 2)); + + $var1 = "Hello"; + $var2 = "hello"; + Assert::eq($var1->iCompare($var2), strcasecmp($var1, $var2)); + Assert::eq($var1->compare($var2), strcmp($var1, $var2)); + + $email = 'name@example.com'; + Assert::eq($email->find('@'), strstr($email, '@')); + Assert::eq($email->iFind('N'), stristr($email, 'N')); + + $text = '

Test paragraph.

Other text'; + Assert::eq($text->stripTags(), strip_tags($text)); + + $text = 'I\'d have a coffee.\nNot a problem.'; + Assert::eq($text->stripCSlashes(), stripcslashes($text)); + + $str = "Is your name O\'reilly?"; + Assert::eq($str->stripSlashes(), stripslashes($str)); + + $mystring1 = 'xyz'; + $mystring2 = 'ABC'; + Assert::true($mystring1->iIndexOf('A') === stripos($mystring1, 'a')); + Assert::true($mystring2->iIndexOf('A') === stripos($mystring2, 'a')); + Assert::eq($mystring2->indexOf('a'), strpos($mystring2, 'a')); + + $mystring = 'Elephpant'; + Assert::true($mystring->lastIndexOf('b') === false); + Assert::true(strrpos($mystring, 'b') === false); + Assert::eq($mystring->iLastIndexOf('E'), strripos($mystring, 'E')); + Assert::eq($mystring->lastCharIndexOf('E'), strrchr($mystring, 'E')); + + $text = "abcdef"; + Assert::eq($text->substr(0, -1), substr($text, 0, -1)); + Assert::eq($text->substrCompare("bc", 1, 2), substr_compare($text, "bc", 1, 2)); + + $text = 'This is a test'; + Assert::eq($text->substrCount('is'), substr_count($text, 'is')); + + $var = 'ABCDEFGH:/MNRPQR/'; + Assert::eq($var->substrReplace('bob', 0), substr_replace($var, 'bob', 0)); + + $var = 'abcdefghijklmn'; + Assert::eq($var->reverse(), strrev($var)); + Assert::eq($var->md5(), md5($var)); + Assert::eq($var->sha1(), sha1($var)); + Assert::eq($var->crc32(), crc32($var)); + Assert::eq($var->hash('sha256'), hash('sha256', $var)); + + $str = 'This is an encoded string'; + $encoded = $str->base64Encode(); + Assert::eq($encoded->base64Decode(), base64_decode(base64_encode($str))); + + $text = 'Data123!@-_ +'; + $encoded = $text->urlEncode(); + Assert::eq($encoded->urlDecode(), urldecode(urlencode($text))); + + $text = 'foo @+%/'; + $encoded = $text->rawUrlEncode(); + Assert::eq($encoded->rawUrlDecode(), rawurldecode(rawurlencode($text))); + + $text = "PHP is the web scripting language of choice."; + $result1 = $text->match("/php/i"); + preg_match("/php/i", $text, $result2); + Assert::eq($result1, $result2); + + $pattern = "/\(? (\d{3})? \)? (?(1) [\-\s] ) \d{3}-\d{4}/x"; + $subject = "Call 555-1212 or 1-800-555-1212"; + $result1 = $subject->matchAll($pattern); + preg_match_all($pattern, $subject, $result2); + Assert::eq($result1, $result2); + + $text = '112'; + Assert::eq($text->isNumeric(), is_numeric($text)); +} +?> +--EXPECT-- diff --git a/tests/aot/string_method/misc.phpt b/tests/aot/string_method/misc.phpt new file mode 100644 index 00000000..a0e5f36d --- /dev/null +++ b/tests/aot/string_method/misc.phpt @@ -0,0 +1,191 @@ +--TEST-- +Universal method call: String type methods +--FILE-- +length()); + var_dump($s->isEmpty()); + $empty = ""; + var_dump($empty->isEmpty()); + + // upper / lower + $s = "Hello World"; + var_dump($s->upper()); + var_dump($s->lower()); + + // lowerFirst / upperFirst + $s = "Hello World"; + var_dump($s->lowerFirst()); + var_dump($s->upperFirst()); + + // upperWords + $s = "hello world!"; + var_dump($s->upperWords()); + + // trim / lTrim / rTrim + $s = " hello world "; + var_dump($s->trim()); + var_dump($s->lTrim()); + var_dump($s->rTrim()); + + // startsWith / endsWith / contains + $s = "hello world"; + var_dump($s->startsWith("hello")); + var_dump($s->startsWith("world")); + var_dump($s->endsWith("world")); + var_dump($s->endsWith("hello")); + var_dump($s->contains("lo wo")); + var_dump($s->contains("xxx")); + + // indexOf / lastIndexOf + $s = "hello world"; + var_dump($s->indexOf("world")); + var_dump($s->indexOf("xxx")); + var_dump($s->lastIndexOf("o")); + + // iCompare / compare + $s = "abc"; + var_dump($s->compare("abc")); + var_dump($s->compare("abd")); + var_dump($s->iCompare("ABC")); + + // reverse / md5 / sha1 / crc32 + $s = "hello"; + var_dump($s->reverse()); + var_dump($s->md5()); + var_dump($s->sha1()); + var_dump($s->crc32()); + + // base64Encode / base64Decode + $s = "hello"; + $encoded = $s->base64Encode(); + var_dump($encoded); + var_dump($encoded->base64Decode()); + + // urlEncode / urlDecode + $s = "hello world"; + $encoded = $s->urlEncode(); + var_dump($encoded); + var_dump($encoded->urlDecode()); + + // equals + $s = "hello"; + var_dump($s->equals("hello")); + var_dump($s->equals("HELLO")); + var_dump($s->equals("HELLO", true)); + + // isNumeric + $n1 = "123"; + $n2 = "12.3"; + $n3 = "abc"; + var_dump($n1->isNumeric()); + 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)); + var_dump($s->substr(0, 5)); + var_dump($s->substr(-5)); + + // split + $s = "a,b,c,d"; + $parts = $s->split(","); + var_dump($parts->count()); + var_dump($parts->get(0)); + var_dump($parts->get(2)); + + // stripTags + $s = "

Hello World

"; + var_dump($s->stripTags("")); + + // addSlashes + $s = "O'Reilly"; + var_dump($s->addSlashes()); + + // toString (identity) + $s = "hello"; + var_dump($s->toString()); + + // toInt / toFloat / toBool + $s = "123"; + var_dump($s->toInt()); + $s = "3.14"; + var_dump($s->toFloat()); + $s = "1"; + var_dump($s->toBool()); + + // substrCount + $s = "hello hello world"; + var_dump($s->substrCount("hello")); + + // wordCount + $s = "hello world test"; + var_dump($s->wordCount()); + + echo "done\n"; +} +?> +--EXPECT-- +int(11) +bool(false) +bool(true) +string(11) "HELLO WORLD" +string(11) "hello world" +string(11) "hello World" +string(11) "Hello World" +string(12) "Hello World!" +string(11) "hello world" +string(13) "hello world " +string(13) " hello world" +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +int(6) +bool(false) +int(7) +int(0) +int(-1) +int(0) +string(5) "olleh" +string(32) "5d41402abc4b2a76b9719d911017c592" +string(40) "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d" +int(907060870) +string(8) "aGVsbG8=" +string(5) "hello" +string(11) "hello+world" +string(11) "hello world" +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(false) +string(11) "hello world" +string(5) "world" +string(5) "hello" +string(5) "world" +int(4) +string(1) "a" +string(1) "c" +string(11) "Hello World" +string(9) "O\'Reilly" +string(5) "hello" +int(123) +float(3.14) +bool(true) +int(2) +int(3) +done diff --git a/tests/aot/universal_method_call.php b/tests/aot/universal_method_call.php new file mode 100644 index 00000000..164d42a5 --- /dev/null +++ b/tests/aot/universal_method_call.php @@ -0,0 +1,66 @@ +add(50); +var_dump($a); + +$a->sub(30); +var_dump($a); + +$a->mul(2); +var_dump($a); + +$a->div(4); +var_dump($a); + +$b = $a->add(10); +var_dump($b); +var_dump($a); // $a also mutated + +// Int conversion +var_dump($a->toString()); +var_dump($a->toFloat()); + +// String methods +$s = "hello world"; +var_dump($s->length()); +var_dump($s->upper()); +var_dump($s->substr(0, 5)); +var_dump($s->split(" ")->count()); + +// Array methods +$arr = []; +$arr->append(100); +$arr->append(200); +$arr->append(300); +var_dump($arr->count()); +var_dump($arr->get(0)); +var_dump($arr->get(1)); +var_dump($arr->exists(0)); +var_dump($arr->empty()); +var_dump($arr->isList()); + +// Array pop +$last = $arr->pop(); +var_dump($last); +var_dump($arr->count()); + +// Array clean +$arr->clean(); +var_dump($arr->empty()); + +// Float methods +$f = 3.14; +$f->add(1.0); +var_dump($f); +var_dump($f->toInt()); + +echo "done\n"; + +} diff --git a/tests/aot/universal_method_call.phpt b/tests/aot/universal_method_call.phpt new file mode 100644 index 00000000..00d4d0ea --- /dev/null +++ b/tests/aot/universal_method_call.phpt @@ -0,0 +1,93 @@ +--TEST-- +Universal method call on native typed variables +--FILE-- +add(50); + var_dump($a); + + $a->sub(30); + var_dump($a); + + $a->mul(2); + var_dump($a); + + $a->div(4); + var_dump($a); + + $b = $a->add(10); + var_dump($b); + var_dump($a); + + // Int conversion + var_dump($a->toString()); + var_dump($a->toFloat()); + + // String methods + $s = "hello world"; + var_dump($s->length()); + var_dump($s->upper()); + var_dump($s->substr(0, 5)); + $words = $s->split(" "); + var_dump($words->count()); + + // Array methods + $arr = []; + $arr->push(100); + $arr->push(200); + $arr->push(300); + var_dump($arr->count()); + var_dump($arr->get(0)); + var_dump($arr->get(1)); + var_dump($arr->keyExists(0)); + var_dump($arr->isEmpty()); + var_dump($arr->isList()); + + // Array pop + $last = $arr->pop(); + var_dump($last); + var_dump($arr->count()); + + // Array clean + $arr->clean(); + var_dump($arr->isEmpty()); + + // Float methods + $f = 3.14; + $f->add(1.0); + var_dump($f); + var_dump($f->toInt()); + + echo "done\n"; +} +?> +--EXPECT-- +int(100) +int(100) +int(100) +int(100) +int(110) +int(100) +string(3) "100" +float(100) +int(11) +string(11) "HELLO WORLD" +string(5) "hello" +int(2) +int(3) +int(100) +int(200) +bool(true) +bool(false) +bool(true) +int(300) +int(2) +bool(true) +float(3.14) +int(3) +done