@ -45,7 +45,7 @@ use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
class Preprocessor extends CompilerBase
abstract class Preprocessor extends CompilerBase
{
/**
* Magic methods forbidden on PHP enums. Enum cases are runtime-managed
@ -986,7 +986,9 @@ class Preprocessor extends CompilerBase
return false;
}
private function hasNoExportAttribute(NodeAbstract $node): bool
private function hasNoExportAttribute(
Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\Stmt\ClassLike $node,
): bool
{
foreach ($node->attrGroups as $group) {
foreach ($group->attrs as $attribute) {
@ -1670,8 +1672,9 @@ class Preprocessor extends CompilerBase
}
}
if (!$this->method & & $this->canOptimizeMultiReturn($v, $functionDef)) {
$functionDef->multiReturnCount = count($v->stmts[array_key_last($v->stmts)]->expr->items);
$multiReturn = !$this->method ? $this->getOptimizableMultiReturn($v, $functionDef) : null;
if ($multiReturn !== null) {
$functionDef->multiReturnCount = count($multiReturn->items);
// The fixed tuple is an internal ABI detail. PHP and ordinary native
// callers continue to observe an array return value.
$functionDef->returnType = Type::ARRAY;
@ -1721,28 +1724,31 @@ class Preprocessor extends CompilerBase
return $functionDef;
}
private function canOptimizeMultiReturn(Node\Stmt\Function_|Node\Stmt\ClassMethod $function, FunctionDef $functionDef): bool
private function getOptimizableMultiReturn(
Node\Stmt\Function_|Node\Stmt\ClassMethod $function,
FunctionDef $functionDef,
): ?Node\Expr\Array_
{
if ($functionDef->stub || $functionDef->generator || $functionDef->returnsByRef
|| ($functionDef->returnType !== Type::ARRAY & & !$functionDef->returnTypeUndeclared)
|| !$function->stmts) {
return false ;
return null ;
}
$return = $function->stmts[array_key_last($function->stmts)] ?? null;
if (!$return instanceof Node\Stmt\Return_ || !$return->expr instanceof Node\Expr\Array_
|| count($return->expr->items) < 2 ) {
return false ;
return null ;
}
$returns = (new NodeFinder())->findInstanceOf($function->stmts, Node\Stmt\Return_::class);
if (count($returns) !== 1) {
return false ;
return null ;
}
foreach ($return->expr->items as $item) {
if ($item === null || $item ->key !== null || $item->unpack || $item->byRef) {
return false ;
if ($item->key !== null || $item->unpack || $item->byRef) {
return null ;
}
$value = $item->value;
if (($value instanceof Node\Expr\Variable & & is_string($value->name))
@ -1750,9 +1756,9 @@ class Preprocessor extends CompilerBase
|| $value instanceof Node\Expr\ConstFetch) {
continue;
}
return false ;
return null ;
}
return true ;
return $return->expr ;
}
protected function prepareFunction(Node\Stmt\ClassMethod|Node\Stmt\Function_ $v): void
@ -1975,51 +1981,50 @@ class Preprocessor extends CompilerBase
$code = '';
foreach ($class->stmts as $v) {
$type = $v->getType();
switch ($type) {
case 'Stmt_ClassConst':
break;
case 'Stmt_Property':
if ($this->classDef->enum) {
$this->fatalError($v, "Enum {$fullClassName} cannot include properties");
}
$this->parseClassPropertyDef($v);
break;
case 'Stmt_TraitUse':
$this->prepareTraitUse($v);
break;
case 'Stmt_Nop':
break;
case 'Stmt_EnumCase':
$caseName = $this->parseIdentifier($v->name);
if (array_key_exists($caseName, $this->classDef->enumCases)
|| $this->classDef->hasConstant($caseName)
) {
$enumName = $this->classDef->getNamespacedName(false);
$this->fatalError($v, "Cannot redefine class constant {$enumName}::{$caseName}");
}
// Keep every backing expression until declaration
// finalization. Literal values also seed enumCases for
// declaration consumers, but code generation only accepts
// values finalized after the complete symbol graph exists.
$this->classDef->enumCases[$caseName] =
$v->expr instanceof Node\Scalar\Int_ || $v->expr instanceof Node\Scalar\String_
? $v->expr->value
: null;
if ($v->expr !== null) {
$this->classDef->enumCaseExpressions[$caseName] = $v->expr;
}
break;
case 'Stmt_ClassMethod':
$this->prepareClassMethod($v, $class);
break;
case 'Stmt_Expression':
$this->foundStrayCode($v);
break;
default:
$this->unsupportedSyntax($v);
break;
if ($v instanceof Node\Stmt\ClassConst || $v instanceof Node\Stmt\Nop) {
continue;
}
if ($v instanceof Node\Stmt\Property) {
if ($this->classDef->enum) {
$this->fatalError($v, "Enum {$fullClassName} cannot include properties");
}
$this->parseClassPropertyDef($v);
continue;
}
if ($v instanceof Node\Stmt\TraitUse) {
$this->prepareTraitUse($v);
continue;
}
if ($v instanceof Node\Stmt\EnumCase) {
$caseName = $this->parseIdentifier($v->name);
if (array_key_exists($caseName, $this->classDef->enumCases)
|| $this->classDef->hasConstant($caseName)
) {
$enumName = $this->classDef->getNamespacedName(false);
$this->fatalError($v, "Cannot redefine class constant {$enumName}::{$caseName}");
}
// Keep every backing expression until declaration
// finalization. Literal values also seed enumCases for
// declaration consumers, but code generation only accepts
// values finalized after the complete symbol graph exists.
$this->classDef->enumCases[$caseName] =
$v->expr instanceof Node\Scalar\Int_ || $v->expr instanceof Node\Scalar\String_
? $v->expr->value
: null;
if ($v->expr !== null) {
$this->classDef->enumCaseExpressions[$caseName] = $v->expr;
}
continue;
}
if ($v instanceof Node\Stmt\ClassMethod) {
$this->prepareClassMethod($v, $class);
continue;
}
if ($v instanceof Node\Stmt\Expression) {
$this->foundStrayCode($v);
continue;
}
$this->unsupportedSyntax($v);
}
// Trait members are later injected into the consuming class for stub
@ -2593,78 +2598,87 @@ class Preprocessor extends CompilerBase
}
$scopeClass ??= $this->getFullClassName();
switch ($node->getType()) {
case 'Scalar_Int':
return 'int';
case 'Scalar_Float':
return 'float';
case 'Scalar_String':
case 'Scalar_InterpolatedString':
case 'Expr_BinaryOp_Concat':
if ($node instanceof Node\Scalar\Int_) {
return 'int';
}
if ($node instanceof Node\Scalar\Float_) {
return 'float';
}
if ($node instanceof Node\Scalar\String_
|| $node instanceof Node\Scalar\InterpolatedString
|| $node instanceof Node\Expr\BinaryOp\Concat
) {
return 'string';
}
if ($node instanceof Node\Expr\Array_) {
return 'array';
}
if ($node instanceof Node\Expr\UnaryMinus) {
return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1);
}
if ($node instanceof Node\Expr\ConstFetch) {
return match (strtolower($node->name->toString())) {
'true' => 'true',
'false' => 'false',
'null' => 'null',
default => null,
};
}
if ($node instanceof Node\Expr\ClassConstFetch) {
if (!$node->class instanceof Node\Name || !$node->name instanceof Node\Identifier) {
return null;
}
$constName = $node->name->toString();
if (strcasecmp($constName, 'class') === 0) {
return 'string';
case 'Expr_Array':
return 'array';
case 'Expr_UnaryMinus':
return $this->detectDefaultValueType($node->expr, $scopeClass, $depth + 1);
case 'Expr_ConstFetch':
return match (strtolower($node->name->toString())) {
'true' => 'true',
'false' => 'false',
'null' => 'null',
default => null,
};
case 'Expr_ClassConstFetch':
if (!$node->class instanceof Node\Name || !$node->name instanceof Node\Identifier) {
return null;
}
$constName = $node->name->toString();
if (strcasecmp($constName, 'class') === 0) {
return 'string';
}
$className = $node->class->toString();
if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) {
$targetClass = $scopeClass;
} elseif (strcasecmp($className, 'parent') === 0) {
$targetClass = $this->getParentClass($scopeClass);
} else {
$targetClass = $this->getNamespacedClassName($className);
}
if ($targetClass === '' || !$this->hasClass($targetClass)) {
return null;
}
$targetDef = $this->getClass($targetClass);
if ($targetDef->enum & & array_key_exists($constName, $targetDef->enumCases)) {
return 'enum:' . $targetDef->getNamespacedName(false);
}
if (!$targetDef->hasConstant($constName)) {
return null;
}
return $this->detectDefaultValueType(
$targetDef->getConstant($constName)->valueExpr,
$targetClass,
$depth + 1
);
default:
try {
$value = (new ConstExprEvaluator(
static function (Node\Expr $expr): never {
throw new \RuntimeException('Unresolved constant expression');
}
))->evaluateDirectly($node);
} catch (\Throwable) {
return null;
}
return match (true) {
is_int($value) => 'int',
is_float($value) => 'float',
is_string($value) => 'string',
$value === true => 'true',
$value === false => 'false',
is_array($value) => 'array',
$value === null => 'null',
default => null,
};
}
$className = $node->class->toString();
if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) {
$targetClass = $scopeClass;
} elseif (strcasecmp($className, 'parent') === 0) {
$targetClass = $this->getParentClass($scopeClass);
} else {
$targetClass = $this->getNamespacedClassName($className);
}
if ($targetClass === '' || !$this->hasClass($targetClass)) {
return null;
}
$targetDef = $this->getClass($targetClass);
if ($targetDef->enum & & array_key_exists($constName, $targetDef->enumCases)) {
return 'enum:' . $targetDef->getNamespacedName(false);
}
if (!$targetDef->hasConstant($constName)) {
return null;
}
return $this->detectDefaultValueType(
$targetDef->getConstant($constName)->valueExpr,
$targetClass,
$depth + 1,
);
}
if (!$node instanceof Node\Expr) {
return null;
}
try {
$value = (new ConstExprEvaluator(
static function (Node\Expr $expr): never {
throw new \RuntimeException('Unresolved constant expression');
},
))->evaluateDirectly($node);
} catch (\Throwable) {
return null;
}
return match (true) {
is_int($value) => 'int',
is_float($value) => 'float',
is_string($value) => 'string',
$value === true => 'true',
$value === false => 'false',
is_array($value) => 'array',
$value === null => 'null',
default => null,
};
}
private function propertyTypeAcceptsEnumCase(NodeAbstract $typeNode, string $enumClass): bool