TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
7.3 KiB
222 lines
7.3 KiB
<?php
|
|
/**
|
|
* This file is part of TypePHP(AOT).
|
|
*
|
|
* @link https://www.swoole.com/aot/
|
|
* @contact service@swoole.com
|
|
*/
|
|
|
|
namespace TypePhp\Analysis;
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\FunctionLike;
|
|
use PhpParser\Node\Stmt;
|
|
|
|
/**
|
|
* Proves the deliberately small set of local Closures which can stay entirely
|
|
* in the generated C++ function. An unknown use is an escape: this analysis
|
|
* must reject it and leave the ordinary Zend Closure lowering untouched.
|
|
*/
|
|
final class LocalClosureAnalyzer
|
|
{
|
|
/** @var array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int}> */
|
|
private array $candidates = [];
|
|
|
|
/** @var array<string, true> */
|
|
private array $invalid = [];
|
|
|
|
/** @var array<string, true> */
|
|
private array $defined = [];
|
|
|
|
/**
|
|
* @param list<Stmt> $statements
|
|
* @return array<string, array{assignment: Expr\Assign, closure: Expr\Closure|Expr\ArrowFunction, calls: int}>
|
|
*/
|
|
public function analyze(array $statements): array
|
|
{
|
|
$this->candidates = [];
|
|
$this->invalid = [];
|
|
$this->defined = [];
|
|
|
|
$duplicateNames = [];
|
|
foreach ($statements as $statement) {
|
|
if (!$statement instanceof Stmt\Expression
|
|
|| !$statement->expr instanceof Expr\Assign
|
|
|| !$statement->expr->var instanceof Expr\Variable
|
|
|| !is_string($statement->expr->var->name)
|
|
|| (!$statement->expr->expr instanceof Expr\Closure
|
|
&& !$statement->expr->expr instanceof Expr\ArrowFunction)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$name = $statement->expr->var->name;
|
|
if (isset($this->candidates[$name])) {
|
|
$duplicateNames[$name] = true;
|
|
continue;
|
|
}
|
|
if (!$this->isSupportedClosure($statement->expr->expr)) {
|
|
continue;
|
|
}
|
|
$this->candidates[$name] = [
|
|
'assignment' => $statement->expr,
|
|
'closure' => $statement->expr->expr,
|
|
'calls' => 0,
|
|
];
|
|
}
|
|
|
|
foreach ($duplicateNames as $name => $_) {
|
|
unset($this->candidates[$name]);
|
|
}
|
|
if ($this->candidates === []) {
|
|
return [];
|
|
}
|
|
|
|
foreach ($statements as $statement) {
|
|
$this->scanNode($statement);
|
|
}
|
|
|
|
foreach ($this->candidates as $name => $candidate) {
|
|
if (isset($this->invalid[$name]) || $candidate['calls'] === 0) {
|
|
unset($this->candidates[$name]);
|
|
}
|
|
}
|
|
return $this->candidates;
|
|
}
|
|
|
|
private function isSupportedClosure(Expr\Closure|Expr\ArrowFunction $closure): bool
|
|
{
|
|
if ($closure->byRef) {
|
|
return false;
|
|
}
|
|
if ($closure->returnType instanceof Node\Identifier
|
|
&& strtolower($closure->returnType->name) === 'never'
|
|
) {
|
|
return false;
|
|
}
|
|
foreach ($closure->params as $parameter) {
|
|
if ($parameter->byRef || $parameter->variadic || $parameter->default !== null) {
|
|
return false;
|
|
}
|
|
}
|
|
$body = $closure instanceof Expr\ArrowFunction ? $closure->expr : $closure->stmts;
|
|
return !$this->containsUnsupportedClosureNode($body, false);
|
|
}
|
|
|
|
private function containsUnsupportedClosureNode(mixed $value, bool $root = true): bool
|
|
{
|
|
foreach (is_array($value) ? $value : [$value] as $node) {
|
|
if (!$node instanceof Node) {
|
|
continue;
|
|
}
|
|
if (!$root && $node instanceof FunctionLike) {
|
|
return true;
|
|
}
|
|
if ($node instanceof Expr\Yield_
|
|
|| $node instanceof Expr\YieldFrom
|
|
|| $node instanceof Stmt\Static_
|
|
|| $node instanceof Stmt\Global_
|
|
) {
|
|
return true;
|
|
}
|
|
if ($node instanceof Expr\FuncCall && $node->name instanceof Node\Name) {
|
|
$name = strtolower(ltrim($node->name->toString(), '\\'));
|
|
if (in_array($name, [
|
|
'func_get_arg',
|
|
'func_get_args',
|
|
'func_num_args',
|
|
'debug_backtrace',
|
|
'debug_print_backtrace',
|
|
], true)) {
|
|
return true;
|
|
}
|
|
}
|
|
foreach ($node->getSubNodeNames() as $field) {
|
|
if ($this->containsUnsupportedClosureNode($node->{$field}, false)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function scanNode(
|
|
mixed $value,
|
|
?Node $parent = null,
|
|
string $parentField = '',
|
|
int $functionDepth = 0,
|
|
): void {
|
|
foreach (is_array($value) ? $value : [$value] as $node) {
|
|
if (!$node instanceof Node) {
|
|
continue;
|
|
}
|
|
|
|
// Textual order is not a dominance proof in the presence of goto:
|
|
// a jump may bypass the lambda initialization or re-enter its
|
|
// scope. Keep all such functions on the Zend Closure path.
|
|
if ($node instanceof Stmt\Goto_ || $node instanceof Stmt\Label) {
|
|
foreach ($this->candidates as $name => $_candidate) {
|
|
$this->invalid[$name] = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($node instanceof Expr\Variable && is_string($node->name)) {
|
|
$this->classifyVariableUse($node->name, $parent, $parentField, $functionDepth);
|
|
}
|
|
|
|
$childFunctionDepth = $functionDepth + ($node instanceof FunctionLike ? 1 : 0);
|
|
foreach ($node->getSubNodeNames() as $field) {
|
|
$this->scanNode($node->{$field}, $node, $field, $childFunctionDepth);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function classifyVariableUse(
|
|
string $name,
|
|
?Node $parent,
|
|
string $parentField,
|
|
int $functionDepth,
|
|
): void {
|
|
if (!isset($this->candidates[$name]) || isset($this->invalid[$name])) {
|
|
return;
|
|
}
|
|
|
|
$candidate = $this->candidates[$name];
|
|
if ($parent instanceof Expr\Assign && $parentField === 'var') {
|
|
if ($parent === $candidate['assignment'] && !isset($this->defined[$name])) {
|
|
$this->defined[$name] = true;
|
|
return;
|
|
}
|
|
$this->invalid[$name] = true;
|
|
return;
|
|
}
|
|
|
|
if ($functionDepth !== 0
|
|
|| !$parent instanceof Expr\FuncCall
|
|
|| $parentField !== 'name'
|
|
|| $parent->isFirstClassCallable()
|
|
|| !isset($this->defined[$name])
|
|
|| !$this->isSupportedDirectCall($parent, count($candidate['closure']->params))
|
|
) {
|
|
$this->invalid[$name] = true;
|
|
return;
|
|
}
|
|
|
|
$this->candidates[$name]['calls']++;
|
|
}
|
|
|
|
private function isSupportedDirectCall(Expr\FuncCall $call, int $parameterCount): bool
|
|
{
|
|
if (count($call->args) !== $parameterCount) {
|
|
return false;
|
|
}
|
|
foreach ($call->args as $argument) {
|
|
if (!$argument instanceof Node\Arg || $argument->unpack || $argument->name !== null) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|