parent
587818d5f9
commit
c9666664aa
12 changed files with 777 additions and 1 deletions
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeDynamicClassExpressionTarget |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
function native_dynamic_class_expression(): void |
||||||
|
{ |
||||||
|
new (NativeDynamicClassExpressionTarget::class)(); |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
function readNativeForwardGlobal(): int |
||||||
|
{ |
||||||
|
global $nativeForwardGlobal; |
||||||
|
return $nativeForwardGlobal->value; |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeForwardGlobalValue |
||||||
|
{ |
||||||
|
public int $value = 42; |
||||||
|
} |
||||||
|
|
||||||
|
class NativeForwardGlobalFactory |
||||||
|
{ |
||||||
|
public static function create(): NativeForwardGlobalValue |
||||||
|
{ |
||||||
|
return new NativeForwardGlobalValue(); |
||||||
|
} |
||||||
|
|
||||||
|
public static function initialize(): void |
||||||
|
{ |
||||||
|
global $nativeForwardGlobal; |
||||||
|
$local = self::create(); |
||||||
|
$nativeForwardGlobal = $local; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,392 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\NativeClass; |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\NodeAbstract; |
||||||
|
|
||||||
|
/** |
||||||
|
* Discovers project-wide global slots which carry Native Object pointers. |
||||||
|
* |
||||||
|
* C++ files are emitted one at a time, while a PHP global can be written in a |
||||||
|
* later file. The slot ABI must therefore be known before the first file is |
||||||
|
* converted. This deliberately small, flow-local analysis only tracks Native |
||||||
|
* pointer identities; ordinary PHP value inference remains in the compiler's |
||||||
|
* normal convert phase. |
||||||
|
*/ |
||||||
|
final class NativeGlobalDiscovery |
||||||
|
{ |
||||||
|
/** @var \Closure(string): ?string */ |
||||||
|
private \Closure $canonicalClass; |
||||||
|
|
||||||
|
/** @var \Closure(string): ?string */ |
||||||
|
private \Closure $nativeClass; |
||||||
|
|
||||||
|
/** @var \Closure(list<string>): ?string */ |
||||||
|
private \Closure $functionReturn; |
||||||
|
|
||||||
|
/** @var \Closure(string, string): ?string */ |
||||||
|
private \Closure $methodReturn; |
||||||
|
|
||||||
|
/** @var \Closure(string, string): ?string */ |
||||||
|
private \Closure $propertyClass; |
||||||
|
|
||||||
|
/** @var \Closure(string, string): ?string */ |
||||||
|
private \Closure $commonClass; |
||||||
|
|
||||||
|
/** @var \Closure(string): ?string */ |
||||||
|
private \Closure $parentClass; |
||||||
|
|
||||||
|
private string $scopeClass = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Closure(string): ?string $canonicalClass |
||||||
|
* @param \Closure(string): ?string $nativeClass |
||||||
|
* @param \Closure(list<string>): ?string $functionReturn |
||||||
|
* @param \Closure(string, string): ?string $methodReturn |
||||||
|
* @param \Closure(string, string): ?string $propertyClass |
||||||
|
* @param \Closure(string, string): ?string $commonClass |
||||||
|
* @param \Closure(string): ?string $parentClass |
||||||
|
*/ |
||||||
|
public function __construct( |
||||||
|
\Closure $canonicalClass, |
||||||
|
\Closure $nativeClass, |
||||||
|
\Closure $functionReturn, |
||||||
|
\Closure $methodReturn, |
||||||
|
\Closure $propertyClass, |
||||||
|
\Closure $commonClass, |
||||||
|
\Closure $parentClass, |
||||||
|
) { |
||||||
|
$this->canonicalClass = $canonicalClass; |
||||||
|
$this->nativeClass = $nativeClass; |
||||||
|
$this->functionReturn = $functionReturn; |
||||||
|
$this->methodReturn = $methodReturn; |
||||||
|
$this->propertyClass = $propertyClass; |
||||||
|
$this->commonClass = $commonClass; |
||||||
|
$this->parentClass = $parentClass; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<Node\Stmt> $statements |
||||||
|
* @return list<array{name: string, class: string, node: NodeAbstract}> |
||||||
|
*/ |
||||||
|
public function discover(array $statements): array |
||||||
|
{ |
||||||
|
$result = []; |
||||||
|
$this->discoverStatements($statements, '', $result); |
||||||
|
return $result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<Node\Stmt> $statements |
||||||
|
* @param list<array{name: string, class: string, node: NodeAbstract}> $result |
||||||
|
*/ |
||||||
|
private function discoverStatements(array $statements, string $class, array &$result): void |
||||||
|
{ |
||||||
|
foreach ($statements as $statement) { |
||||||
|
if ($statement instanceof Node\Stmt\Namespace_) { |
||||||
|
$this->discoverStatements($statement->stmts, '', $result); |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($statement instanceof Node\Stmt\Function_) { |
||||||
|
$this->discoverFunction($statement, '', $result); |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($statement instanceof Node\Stmt\ClassLike) { |
||||||
|
$className = isset($statement->namespacedName) |
||||||
|
? $statement->namespacedName->toString() |
||||||
|
: ($statement->name?->toString() ?? ''); |
||||||
|
foreach ($statement->getMethods() as $method) { |
||||||
|
$this->discoverFunction($method, $className, $result); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<array{name: string, class: string, node: NodeAbstract}> $result |
||||||
|
*/ |
||||||
|
private function discoverFunction( |
||||||
|
Node\Stmt\Function_|Node\Stmt\ClassMethod $function, |
||||||
|
string $class, |
||||||
|
array &$result, |
||||||
|
): void { |
||||||
|
if ($function->stmts === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$globals = []; |
||||||
|
$this->collectGlobals($function->stmts, $globals); |
||||||
|
if ($globals === [] && !$this->containsGlobalsArray($function->stmts)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$locals = []; |
||||||
|
$previousScope = $this->scopeClass; |
||||||
|
$thisClass = ($this->canonicalClass)($class); |
||||||
|
$this->scopeClass = $thisClass ?? ''; |
||||||
|
if ($thisClass !== null) { |
||||||
|
$locals['this'] = $thisClass; |
||||||
|
} |
||||||
|
foreach ($function->params as $parameter) { |
||||||
|
if (!$parameter->var instanceof Node\Expr\Variable || !is_string($parameter->var->name)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$parameterClass = $this->classFromType($parameter->type); |
||||||
|
if ($parameterClass !== null) { |
||||||
|
$locals[$parameter->var->name] = $parameterClass; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$this->analyzeNodes($function->stmts, $globals, $locals, $result); |
||||||
|
$this->scopeClass = $previousScope; |
||||||
|
} |
||||||
|
|
||||||
|
/** @param array<string, true> $globals */ |
||||||
|
private function collectGlobals(mixed $node, array &$globals): void |
||||||
|
{ |
||||||
|
if ($node === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (is_array($node)) { |
||||||
|
foreach ($node as $item) { |
||||||
|
$this->collectGlobals($item, $globals); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if ($node instanceof Node\Stmt\Global_) { |
||||||
|
foreach ($node->vars as $variable) { |
||||||
|
if ($variable instanceof Node\Expr\Variable && is_string($variable->name)) { |
||||||
|
$globals[$variable->name] = true; |
||||||
|
} |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
foreach ($node->getSubNodeNames() as $name) { |
||||||
|
$this->collectGlobals($node->{$name}, $globals); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function containsGlobalsArray(mixed $node): bool |
||||||
|
{ |
||||||
|
if ($node === null) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (is_array($node)) { |
||||||
|
foreach ($node as $item) { |
||||||
|
if ($this->containsGlobalsArray($item)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($this->globalArrayName($node) !== null) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
foreach ($node->getSubNodeNames() as $name) { |
||||||
|
if ($this->containsGlobalsArray($node->{$name})) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param array<string, true> $globals |
||||||
|
* @param array<string, string> $locals |
||||||
|
* @param list<array{name: string, class: string, node: NodeAbstract}> $result |
||||||
|
*/ |
||||||
|
private function analyzeNodes( |
||||||
|
mixed $node, |
||||||
|
array $globals, |
||||||
|
array &$locals, |
||||||
|
array &$result, |
||||||
|
): void { |
||||||
|
if ($node === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (is_array($node)) { |
||||||
|
foreach ($node as $item) { |
||||||
|
$this->analyzeNodes($item, $globals, $locals, $result); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!$node instanceof NodeAbstract || $node instanceof Node\FunctionLike) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if ($node instanceof Node\Expr\Assign) { |
||||||
|
$this->analyzeNodes($node->expr, $globals, $locals, $result); |
||||||
|
$class = $this->inferClass($node->expr, $locals); |
||||||
|
$globalName = $this->assignmentGlobalName($node->var, $globals); |
||||||
|
$nativeClass = $class === null ? null : ($this->nativeClass)($class); |
||||||
|
if ($globalName !== null && $nativeClass !== null) { |
||||||
|
$result[] = ['name' => $globalName, 'class' => $nativeClass, 'node' => $node]; |
||||||
|
} elseif ($node->var instanceof Node\Expr\Variable && is_string($node->var->name)) { |
||||||
|
if ($class !== null) { |
||||||
|
$locals[$node->var->name] = $class; |
||||||
|
} elseif (!$this->isNull($node->expr)) { |
||||||
|
unset($locals[$node->var->name]); |
||||||
|
} |
||||||
|
} |
||||||
|
$this->analyzeNodes($node->var, $globals, $locals, $result); |
||||||
|
return; |
||||||
|
} |
||||||
|
foreach ($node->getSubNodeNames() as $name) { |
||||||
|
$this->analyzeNodes($node->{$name}, $globals, $locals, $result); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** @param array<string, string> $locals */ |
||||||
|
private function inferClass(NodeAbstract $expression, array $locals): ?string |
||||||
|
{ |
||||||
|
if ($expression instanceof Node\Expr\Assign) { |
||||||
|
return $this->inferClass($expression->expr, $locals); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\New_ && $expression->class instanceof Node\Name) { |
||||||
|
return $this->resolvedClass($expression->class); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\Variable && is_string($expression->name)) { |
||||||
|
return $locals[$expression->name] ?? null; |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\Clone_ |
||||||
|
|| $expression instanceof Node\Expr\ErrorSuppress |
||||||
|
) { |
||||||
|
return $this->inferClass($expression->expr, $locals); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\FuncCall && $expression->name instanceof Node\Name) { |
||||||
|
return ($this->functionReturn)($this->resolvedNameCandidates($expression->name)); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\StaticCall |
||||||
|
&& $expression->class instanceof Node\Name |
||||||
|
&& $expression->name instanceof Node\Identifier |
||||||
|
) { |
||||||
|
$class = $this->resolvedClass($expression->class); |
||||||
|
return $class === null ? null : ($this->methodReturn)($class, $expression->name->toString()); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\MethodCall |
||||||
|
&& $expression->name instanceof Node\Identifier |
||||||
|
) { |
||||||
|
$class = $this->inferClass($expression->var, $locals); |
||||||
|
return $class === null ? null : ($this->methodReturn)($class, $expression->name->toString()); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\PropertyFetch |
||||||
|
&& $expression->name instanceof Node\Identifier |
||||||
|
) { |
||||||
|
$class = $this->inferClass($expression->var, $locals); |
||||||
|
return $class === null ? null : ($this->propertyClass)($class, $expression->name->toString()); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\Ternary) { |
||||||
|
$if = $expression->if === null |
||||||
|
? $this->inferClass($expression->cond, $locals) |
||||||
|
: $this->inferClass($expression->if, $locals); |
||||||
|
return $this->mergeClasses($if, $this->inferClass($expression->else, $locals)); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\BinaryOp\Coalesce) { |
||||||
|
return $this->mergeClasses( |
||||||
|
$this->inferClass($expression->left, $locals), |
||||||
|
$this->inferClass($expression->right, $locals), |
||||||
|
); |
||||||
|
} |
||||||
|
if ($expression instanceof Node\Expr\Match_) { |
||||||
|
$class = null; |
||||||
|
foreach ($expression->arms as $arm) { |
||||||
|
$class = $this->mergeClasses($class, $this->inferClass($arm->body, $locals)); |
||||||
|
} |
||||||
|
return $class; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private function mergeClasses(?string $left, ?string $right): ?string |
||||||
|
{ |
||||||
|
if ($left === null) { |
||||||
|
return $right; |
||||||
|
} |
||||||
|
if ($right === null) { |
||||||
|
return $left; |
||||||
|
} |
||||||
|
return ($this->commonClass)($left, $right); |
||||||
|
} |
||||||
|
|
||||||
|
private function classFromType(?NodeAbstract $type): ?string |
||||||
|
{ |
||||||
|
if ($type instanceof Node\NullableType) { |
||||||
|
$type = $type->type; |
||||||
|
} |
||||||
|
return $type instanceof Node\Name |
||||||
|
? $this->resolvedClass($type) |
||||||
|
: null; |
||||||
|
} |
||||||
|
|
||||||
|
/** @param array<string, true> $globals */ |
||||||
|
private function assignmentGlobalName(NodeAbstract $target, array $globals): ?string |
||||||
|
{ |
||||||
|
if ($target instanceof Node\Expr\Variable |
||||||
|
&& is_string($target->name) |
||||||
|
&& isset($globals[$target->name]) |
||||||
|
) { |
||||||
|
return $target->name; |
||||||
|
} |
||||||
|
return $this->globalArrayName($target); |
||||||
|
} |
||||||
|
|
||||||
|
private function globalArrayName(NodeAbstract $node): ?string |
||||||
|
{ |
||||||
|
if (!$node instanceof Node\Expr\ArrayDimFetch |
||||||
|
|| !$node->var instanceof Node\Expr\Variable |
||||||
|
|| $node->var->name !== 'GLOBALS' |
||||||
|
|| !$node->dim instanceof Node\Scalar\String_ |
||||||
|
) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return $node->dim->value; |
||||||
|
} |
||||||
|
|
||||||
|
private function isNull(NodeAbstract $expression): bool |
||||||
|
{ |
||||||
|
return $expression instanceof Node\Expr\ConstFetch |
||||||
|
&& strtolower($expression->name->toString()) === 'null'; |
||||||
|
} |
||||||
|
|
||||||
|
private function resolvedName(Node\Name $name): string |
||||||
|
{ |
||||||
|
$resolved = $name->getAttribute('resolvedName'); |
||||||
|
return ltrim($resolved instanceof Node\Name ? $resolved->toString() : $name->toString(), '\\'); |
||||||
|
} |
||||||
|
|
||||||
|
private function resolvedClass(Node\Name $name): ?string |
||||||
|
{ |
||||||
|
$keyword = strtolower($name->toString()); |
||||||
|
if ($keyword === 'self' || $keyword === 'static') { |
||||||
|
return $this->scopeClass !== '' ? $this->scopeClass : null; |
||||||
|
} |
||||||
|
if ($keyword === 'parent') { |
||||||
|
return $this->scopeClass !== '' ? ($this->parentClass)($this->scopeClass) : null; |
||||||
|
} |
||||||
|
return ($this->canonicalClass)($this->resolvedName($name)); |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<string> */ |
||||||
|
private function resolvedNameCandidates(Node\Name $name): array |
||||||
|
{ |
||||||
|
$names = [$this->resolvedName($name)]; |
||||||
|
$fallback = $name->getAttribute('fallbackName'); |
||||||
|
if ($fallback instanceof Node\Name) { |
||||||
|
$names[] = ltrim($fallback->toString(), '\\'); |
||||||
|
} |
||||||
|
$names[] = ltrim($name->toString(), '\\'); |
||||||
|
return array_values(array_unique($names)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,82 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: clone preserves PHPX field semantics and traces shared native children |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeCloneChild |
||||||
|
{ |
||||||
|
public string $name = 'child'; |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeCloneFields |
||||||
|
{ |
||||||
|
public string $name = 'source'; |
||||||
|
public array $values = [1]; |
||||||
|
public object $object; |
||||||
|
public Stream $stream; |
||||||
|
public mixed $mixed = null; |
||||||
|
public NativeCloneChild $child; |
||||||
|
} |
||||||
|
|
||||||
|
function createClone(): NativeCloneFields |
||||||
|
{ |
||||||
|
$source = new NativeCloneFields(); |
||||||
|
$source->object = (object) ['value' => 1]; |
||||||
|
$source->stream = fopen('php://memory', 'w+'); |
||||||
|
$source->mixed = ['mixed']; |
||||||
|
$source->child = new NativeCloneChild(); |
||||||
|
|
||||||
|
$copy = clone $source; |
||||||
|
$copy->name = 'copy'; |
||||||
|
$copy->values[] = 2; |
||||||
|
$copy->object->value = 2; |
||||||
|
fwrite($copy->stream, 'shared'); |
||||||
|
$copy->mixed[] = 'copy'; |
||||||
|
|
||||||
|
// Allocate enough garbage to exercise tracing of the cloned Native child. |
||||||
|
for ($i = 0; $i < 10000; $i++) { |
||||||
|
new NativeCloneChild(); |
||||||
|
} |
||||||
|
|
||||||
|
var_dump($source->name, $source->values, $source->object->value, $source->mixed); |
||||||
|
rewind($source->stream); |
||||||
|
echo stream_get_contents($source->stream), "\n"; |
||||||
|
echo $copy->child->name, "\n"; |
||||||
|
return $copy; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$copy = createClone(); |
||||||
|
var_dump($copy->name, $copy->values, $copy->object->value, $copy->mixed); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(6) "source" |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
int(1) |
||||||
|
} |
||||||
|
int(2) |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(5) "mixed" |
||||||
|
} |
||||||
|
shared |
||||||
|
child |
||||||
|
string(4) "copy" |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
int(1) |
||||||
|
[1]=> |
||||||
|
int(2) |
||||||
|
} |
||||||
|
int(2) |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(5) "mixed" |
||||||
|
[1]=> |
||||||
|
string(4) "copy" |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: an unreachable clone whose __clone throws is finalized by the GC |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeFailedCloneFinalizer |
||||||
|
{ |
||||||
|
public string $kind = 'source'; |
||||||
|
|
||||||
|
public function __clone(): void |
||||||
|
{ |
||||||
|
$this->kind = 'clone'; |
||||||
|
throw new RuntimeException('clone failed'); |
||||||
|
} |
||||||
|
|
||||||
|
public function __destruct() |
||||||
|
{ |
||||||
|
global $finalizedKinds; |
||||||
|
$finalizedKinds[] = $this->kind; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeFailedClonePressure |
||||||
|
{ |
||||||
|
public int $value; |
||||||
|
} |
||||||
|
|
||||||
|
function forceFailedCloneCollection(): void |
||||||
|
{ |
||||||
|
for ($i = 0; $i < 400000; $i++) { |
||||||
|
new NativeFailedClonePressure(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
global $finalizedKinds; |
||||||
|
$finalizedKinds = []; |
||||||
|
|
||||||
|
$source = new NativeFailedCloneFinalizer(); |
||||||
|
try { |
||||||
|
clone $source; |
||||||
|
} catch (RuntimeException $error) { |
||||||
|
echo $error->getMessage(), "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
forceFailedCloneCollection(); |
||||||
|
var_dump($finalizedKinds); |
||||||
|
echo $source->kind, "\n"; |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
clone failed |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(5) "clone" |
||||||
|
} |
||||||
|
source |
||||||
Loading…
Reference in new issue