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.
373 lines
13 KiB
373 lines
13 KiB
<?php
|
|
/**
|
|
* This file is part of Swoole-Compiler(AOT).
|
|
*
|
|
* @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
|
|
{
|
|
private NativeGlobalTypeResolver $resolver;
|
|
|
|
/** @var array<string, string> */
|
|
private array $functionReturns;
|
|
private string $scopeClass = '';
|
|
|
|
/**
|
|
* @param array<string, string> $functionReturns
|
|
*/
|
|
public function __construct(
|
|
NativeGlobalTypeResolver $resolver,
|
|
array $functionReturns,
|
|
) {
|
|
$this->resolver = $resolver;
|
|
$this->functionReturns = $functionReturns;
|
|
}
|
|
|
|
/**
|
|
* @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->resolver->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 || $node instanceof Node\Expr\AssignOp\Coalesce) {
|
|
$this->analyzeNodes($node->expr, $globals, $locals, $result);
|
|
$class = $this->inferClass($node->expr, $locals);
|
|
$globalName = $this->assignmentGlobalName($node->var, $globals);
|
|
$nativeClass = $class === null ? null : $this->resolver->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 || $expression instanceof Node\Expr\AssignOp\Coalesce) {
|
|
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) {
|
|
foreach ($this->resolvedNameCandidates($expression->name) as $name) {
|
|
$key = strtolower(ltrim($name, '\\'));
|
|
if (isset($this->functionReturns[$key])) {
|
|
return $this->functionReturns[$key];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
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->resolver->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->resolver->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->resolver->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->resolver->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->resolver->parentClass($this->scopeClass)
|
|
: null;
|
|
}
|
|
return $this->resolver->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));
|
|
}
|
|
}
|
|
|