feat(core): 通用方法实现,纯静态,零成本抽象

pull/1/head
韩天峰 3 months ago
parent 0c77bc53e4
commit 6309346c9a
  1. 12
      examples/methods/array.php
  2. 5
      examples/ref/stream.php
  3. 7
      phpunit/code/universal-method-int-undefined.php
  4. 7
      phpunit/code/universal-method-int-wrong-args.php
  5. 6
      phpunit/code/universal-method-literal-call.php
  6. 6
      phpunit/code/universal-method-no-native-types.php
  7. 19
      phpunit/src/UniversalMethodCallTest.php
  8. 139
      src/Php/CompilerBase.php
  9. 418
      src/Php/UniversalMethodCall.php
  10. 5
      src/polyfills.php
  11. 24
      tests/aot/array_method/0.phpt
  12. 18
      tests/aot/array_method/1.phpt
  13. 29
      tests/aot/array_method/2.phpt
  14. 300
      tests/aot/array_method/3.phpt
  15. 175
      tests/aot/array_method/method.phpt
  16. 93
      tests/aot/array_method/misc.phpt
  17. 35
      tests/aot/stream_method/0.phpt
  18. 60
      tests/aot/stream_method/1.phpt
  19. 22
      tests/aot/string_method/0.phpt
  20. 18
      tests/aot/string_method/1.phpt
  21. 17
      tests/aot/string_method/json.phpt
  22. 20
      tests/aot/string_method/marshal.phpt
  23. 25
      tests/aot/string_method/match.phpt
  24. 66
      tests/aot/string_method/mbstring.phpt
  25. 167
      tests/aot/string_method/method.phpt
  26. 191
      tests/aot/string_method/misc.phpt
  27. 66
      tests/aot/universal_method_call.php
  28. 93
      tests/aot/universal_method_call.phpt

@ -0,0 +1,12 @@
<?php
$array = [1, 3, 4];
$array->push(5);
$array->slice(1, 2)->find(3);
$a = 100;
$a->add(99)->mul(2)->sub(100);
$s = "hello";
$s->upper();

@ -0,0 +1,5 @@
<?php
$fp = fopen("php://stdin", "r");
$stream =

@ -0,0 +1,7 @@
<?php
use native_types;
function main()
{
$a = 100;
$a->unknownMethod();
}

@ -0,0 +1,7 @@
<?php
use native_types;
function main()
{
$a = 100;
$a->add();
}

@ -0,0 +1,6 @@
<?php
use native_types;
function main()
{
(100)->add(99);
}

@ -0,0 +1,6 @@
<?php
function main()
{
$a = 100;
$a->add(1);
}

@ -0,0 +1,19 @@
<?php
class UniversalMethodCallTest extends \BaseTest
{
public function testUnknownMethodOnInt()
{
$this->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');
}
}

@ -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,9 +2290,15 @@ 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);
// 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);
@ -2255,10 +2307,53 @@ class CompilerBase extends \PhpAot\Core\Translator
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->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)) {
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');
@ -2266,6 +2361,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return $info['type'];
}
}
}
break;
case 'Expr_ArrayDimFetch':
if ($this->isStdArrayExpr($expr)) {
@ -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) {

@ -0,0 +1,418 @@
<?php
namespace PhpAot\Php;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
trait UniversalMethodCall
{
protected const array UNIVERSAL_METHODS = [
self::TYPE_INT => [
'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;
}
}

@ -95,3 +95,8 @@ function any(mixed $var): mixed
{
return $var;
}
function stream_cast($stream)
{
return $stream;
}

@ -0,0 +1,24 @@
--TEST--
array_method: 0
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$array = [];
$array[0] = 1;
$array[1] = 2;
$array[2] = 3;
$array[3] = 999;
Assert::false($array->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--

@ -0,0 +1,18 @@
--TEST--
array_method: 1
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$array = ["lemon", "orange", "banana", "apple"];
$sorted_array = array("apple", "banana", "lemon", "orange",);
$array->sort(SORT_NATURAL | SORT_FLAG_CASE);
Assert::same($array, $sorted_array);
echo "done\n";
}
?>
--EXPECT--
done

@ -0,0 +1,29 @@
--TEST--
Universal method: array_method/2 (shift/unshift on Ref)
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$array = array("orange", "banana", "apple", "raspberry");
$stack = $array;
Assert::eq($stack->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

@ -0,0 +1,300 @@
--TEST--
Universal method call: Array type methods
--FILE--
<?php
function _even_filter($v) { return $v % 2 == 0; }
function main()
{
// push / count / isEmpty
$arr = [];
var_dump($arr->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

@ -0,0 +1,175 @@
--TEST--
array_method: all array methods test
--FILE--
<?php
function _odd($var)
{
return $var & 1;
}
function _cube($n)
{
return ($n * $n * $n);
}
function _sum($carry, $item)
{
$carry += $item;
return $carry;
}
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$array = array("FirSt" => 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--

@ -0,0 +1,93 @@
--TEST--
Universal method call on native typed variables
--FILE--
<?php
use native_types;
function main()
{
// Int methods
$a = 100;
$a->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

@ -0,0 +1,35 @@
--TEST--
stream_method: 0
--SKIPIF--
--FILE--
<?php
require __DIR__ . '/../../../src/Assert.php';
$filepath = "/tmp/test.txt";
$fp = fopen($filepath, "w+");
$rdata = random_bytes(1024);
Assert::greaterThan($fp->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--

@ -0,0 +1,60 @@
--TEST--
Universal method call on stream resources with null guard
--FILE--
<?php
function main()
{
// Test 1: fopen()->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

@ -0,0 +1,22 @@
--TEST--
string_method: 0
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$string = 'hello world, this is a test string';
Assert::false($string->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--

@ -0,0 +1,18 @@
--TEST--
string_method: 1
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$str = "first=value&arr[]=foo+bar&arr[]=baz";
$output = $str->parseStr();
Assert::eq($output['first'], 'value');
Assert::eq($output['arr'][0], 'foo bar');
Assert::eq($output['arr'][1], 'baz');
}
?>
--EXPECT--

@ -0,0 +1,17 @@
--TEST--
string_method: json
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$array = ['a' => 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--

@ -0,0 +1,20 @@
--TEST--
string_method: marshal
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$array = [];
$array->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--

@ -0,0 +1,25 @@
--TEST--
string_method: match
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$str = 'foobarbaz';
$regex1 = '/(foo)(bar)(baz)/';
$matches = $str->match($regex1, PREG_OFFSET_CAPTURE);
preg_match($regex1, $str, $matches2, PREG_OFFSET_CAPTURE);
Assert::eq($matches, $matches2);
$html = "<b>bold text</b><a href=howdy.html>click me</a>";
$regex2 = "/(<([\w]+)[^>]*>)(.*?)(<\/\\2>)/";
preg_match_all($regex2, $html, $matches2, PREG_SET_ORDER);
$matches = $html->matchAll($regex2, PREG_SET_ORDER);
Assert::eq($matches, $matches2);
}
?>
--EXPECT--

@ -0,0 +1,66 @@
--TEST--
string_method: mbstring
--SKIPIF--
<?php
if (!extension_loaded("mbstring")) exit("skip mbstring extension not loaded");
?>
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$text = 'bbbb大声向宇宙呐喊bbbb';
Assert::eq($text->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--

@ -0,0 +1,167 @@
--TEST--
string_method: all string methods test
--FILE--
<?php
function main()
{
require __DIR__ . '/../../../src/Assert.php';
$text = 'aaaaaa';
Assert::eq($text->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 <b>dog</b> now";
$encoded = $text->htmlEntityEncode();
Assert::eq($encoded->htmlEntityDecode(), html_entity_decode(htmlentities($text)));
$text = "<a href='test'>Test</a>";
$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 = "<body text=%BODY%>";
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 = '<p>Test paragraph.</p><!-- Comment --> <a href="#fragment">Other text</a>';
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--

@ -0,0 +1,191 @@
--TEST--
Universal method call: String type methods
--FILE--
<?php
function main()
{
// length / isEmpty
$s = "hello world";
var_dump($s->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 = "<p>Hello <b>World</b></p>";
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

@ -0,0 +1,66 @@
<?php
use native_types;
function main() {
// Int methods
$a = 100;
$a->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";
}

@ -0,0 +1,93 @@
--TEST--
Universal method call on native typed variables
--FILE--
<?php
use native_types;
function main()
{
// Int methods
$a = 100;
$a->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
Loading…
Cancel
Save