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.
 
 

740 lines
25 KiB

<?php
/**
* SSA-based type narrowing optimizer.
*
* Uses SSA/e-SSA analysis to narrow local variable types from php::Var
* to C++ native types (php::Int, php::Float) when all definitions
* provably produce the same type and no dangerous operations exist.
*/
namespace TypePhp\Optimizer;
use TypePhp\Type;
use TypePhp\Analysis\SsaBuilder;
use TypePhp\Analysis\SsaFlags;
use TypePhp\Analysis\SsaVar;
use PhpParser\Node;
use PhpParser\NodeAbstract;
trait SsaTypeOptimizer
{
/**
* Binary ops that are safe while reading a narrowed int. Arithmetic is
* excluded because it may overflow to float in PHP.
*/
protected const array SAFE_INT_BINARY_OPS = [
'Expr_BinaryOp_BitwiseAnd' => true,
'Expr_BinaryOp_BitwiseOr' => true,
'Expr_BinaryOp_BitwiseXor' => true,
'Expr_BinaryOp_ShiftLeft' => true,
'Expr_BinaryOp_ShiftRight' => true,
'Expr_BinaryOp_Mod' => true,
'Expr_BinaryOp_Equal' => true,
'Expr_BinaryOp_NotEqual' => true,
'Expr_BinaryOp_Identical' => true,
'Expr_BinaryOp_NotIdentical' => true,
'Expr_BinaryOp_Greater' => true,
'Expr_BinaryOp_GreaterOrEqual' => true,
'Expr_BinaryOp_Smaller' => true,
'Expr_BinaryOp_SmallerOrEqual' => true,
'Expr_BinaryOp_Spaceship' => true,
'Expr_BinaryOp_BooleanAnd' => true,
'Expr_BinaryOp_BooleanOr' => true,
'Expr_BinaryOp_LogicalAnd' => true,
'Expr_BinaryOp_LogicalOr' => true,
'Expr_BinaryOp_Coalesce' => true,
];
/**
* Ops that force numeric operands through integer-only PHP semantics.
* They are unsafe for variables narrowed to php::Float.
*/
protected const array FLOAT_INT_ONLY_OPS = [
'Expr_BinaryOp_BitwiseAnd' => true,
'Expr_BinaryOp_BitwiseOr' => true,
'Expr_BinaryOp_BitwiseXor' => true,
'Expr_BinaryOp_ShiftLeft' => true,
'Expr_BinaryOp_ShiftRight' => true,
'Expr_BinaryOp_Mod' => true,
'Expr_AssignOp_BitwiseAnd' => true,
'Expr_AssignOp_BitwiseOr' => true,
'Expr_AssignOp_BitwiseXor' => true,
'Expr_AssignOp_ShiftLeft' => true,
'Expr_AssignOp_ShiftRight' => true,
'Expr_AssignOp_Mod' => true,
];
/**
* Use SSA analysis to narrow local variable types to C++ native types.
*
* For each variable, if ALL definitions produce the same narrow type
* (int, float), and the variable is never referenced, escaped,
* killed, or defined by a φ function with mixed sources, pre-set the
* type in localVars so genScopeVarDecl emits the narrow C++ type.
*/
protected function optimizeVarTypes(SsaBuilder $ssa): void
{
if (empty($ssa->ssaVars)) {
return;
}
$narrowableTypes = [
Type::INT => true,
Type::FLOAT => true,
];
// Group SSA vars by original variable name
$groups = [];
foreach ($ssa->ssaVars as $ssaVar) {
$name = $ssaVar->origName;
if (!isset($groups[$name])) {
$groups[$name] = [];
}
$groups[$name][] = $ssaVar;
}
// Type detection may inspect RHS expressions that read variables
// defined by earlier assignments. Code generation normally registers
// those locals while parsing assignments, but SSA optimization runs
// before that parse pass, so seed the optimization context with the
// SSA-defined local names as generic Vars.
foreach (array_keys($groups) as $name) {
$varName = $this->escapeVarName($name);
if (!isset($this->context->arguments[$varName]) && !$this->hasVar($varName)) {
$this->context->localVars[$varName] = Type::VAR;
}
}
foreach ($groups as $varName => $varList) {
$varName = $this->escapeVarName($varName);
// Skip parameters — they already have declared types
if (isset($this->context->arguments[$varName])) {
continue;
}
// Skip if any SSA var has dangerous flags
$hasPhi = false;
$hasDanger = false;
$narrowedType = null;
$nonNarrowableType = null;
foreach ($varList as $ssaVar) {
if ($ssaVar->flags & SsaFlags::PHI) {
$hasPhi = true;
continue;
}
if ($ssaVar->flags & (SsaFlags::REFERENCE | SsaFlags::ESCAPED | SsaFlags::KILLED)) {
$hasDanger = true;
break;
}
$defType = $this->detectSsaDefType($ssaVar);
if ($defType === null) {
$hasDanger = true;
break;
}
// Non-narrowable types (BigInt/BigFloat/Decimal/Stream/objects etc.)
// are not dangerous — they just can't be narrowed. Record the type
// so dependent SSA variables can resolve it later.
if (!isset($narrowableTypes[$defType])) {
if ($nonNarrowableType === null) {
$nonNarrowableType = $defType;
} elseif ($nonNarrowableType !== $defType) {
// Mixed non-narrowable types — can't determine a single type
$nonNarrowableType = Type::VAR;
}
continue;
}
if ($narrowedType === null) {
$narrowedType = $defType;
} elseif ($narrowedType !== $defType) {
$hasDanger = true;
break;
}
}
if ($hasDanger) {
continue;
}
// Mixed narrowable and non-narrowable types (e.g. $x = [1,2] then $x = 42)
// — can't safely narrow.
if ($narrowedType !== null && $nonNarrowableType !== null && $nonNarrowableType !== Type::VAR) {
continue;
}
if ($narrowedType === null) {
// No narrowable type found, but register Big* types so dependent
// SSA variables can resolve them (these types have no extra metadata).
if (
$nonNarrowableType !== null
&& in_array($nonNarrowableType, [Type::BIGINT, Type::DECIMAL, Type::BIGFLOAT, Type::STREAM], true)
) {
$this->context->localVars[$varName] = $nonNarrowableType;
}
continue;
}
// Scan for operations that SSA definition types alone can't detect
$functionStmts = $ssa->getStmts();
if ($functionStmts) {
if ($narrowedType === Type::INT && $this->hasDangerousIntOps($varName, $functionStmts)) {
continue;
}
if ($narrowedType === Type::FLOAT && $this->hasDangerousFloatOps($varName, $functionStmts)) {
continue;
}
}
// Check φ-function sources for mixed types
if ($hasPhi && $this->phiSourcesHaveMixedTypes($ssa, $varList, $narrowedType)) {
continue;
}
// Apply narrowing — bypass getNativeType(): SSA has PROVEN the type.
$this->context->localVars[$varName] = $narrowedType;
}
}
/**
* Detect the type produced by a single SSA variable definition.
* Returns null if the type cannot be determined from the AST.
*/
protected function detectSsaDefType(SsaVar $ssaVar): ?string
{
$def = $ssaVar->definition;
if (!$def) {
return null;
}
if ($def instanceof Node\Stmt\Expression && $def->expr instanceof Node\Expr\Assign) {
$expr = $def->expr->expr;
$type = $this->detectTypeOfExpr($expr);
if ($type === Type::INT && !$this->isSafeSsaIntExpr($expr)) {
return null;
}
return $type;
}
if ($def instanceof Node\Stmt\Expression && $def->expr instanceof Node\Expr\AssignOp) {
return $this->detectAssignOpDefType($def->expr);
}
if ($def instanceof Node\Stmt\Expression
&& ($def->expr instanceof Node\Expr\PreInc || $def->expr instanceof Node\Expr\PostInc
|| $def->expr instanceof Node\Expr\PreDec || $def->expr instanceof Node\Expr\PostDec)) {
return null;
}
if ($def instanceof Node\Stmt\Foreach_) {
return null;
}
if ($def instanceof Node\Stmt\Catch_) {
return Type::OBJECT;
}
if ($def instanceof Node\Stmt\Static_) {
foreach ($def->vars as $staticVar) {
if ($staticVar->var->name === $ssaVar->origName && $staticVar->default) {
return $this->detectTypeOfExpr($staticVar->default);
}
}
return null;
}
return null;
}
protected function detectAssignOpDefType(Node\Expr\AssignOp $expr): ?string
{
if ($expr instanceof Node\Expr\AssignOp\Div) {
return Type::FLOAT;
}
if ($expr instanceof Node\Expr\AssignOp\Concat) {
return Type::STR;
}
if ($expr instanceof Node\Expr\AssignOp\Pow) {
return null;
}
if ($expr instanceof Node\Expr\AssignOp\Plus
|| $expr instanceof Node\Expr\AssignOp\Minus
|| $expr instanceof Node\Expr\AssignOp\Mul) {
$rhsType = $this->detectTypeOfExpr($expr->expr);
if ($rhsType === Type::FLOAT) {
return Type::FLOAT;
}
if ($rhsType === Type::INT) {
return null;
}
return $rhsType;
}
return null;
}
/**
* Return true only for int-producing expressions whose C++ native-int
* evaluation matches PHP without relying on range analysis.
*/
protected function isSafeSsaIntExpr(NodeAbstract $expr): bool
{
if ($expr instanceof Node\Scalar\LNumber) {
return true;
}
if ($expr instanceof Node\Expr\Cast\Int_) {
return true;
}
if ($expr instanceof Node\Expr\ConstFetch) {
return $this->detectConstType($expr) === Type::INT;
}
if ($expr instanceof Node\Expr\BitwiseNot) {
return $this->isSafeSsaIntExpr($expr->expr);
}
if ($expr instanceof Node\Expr\BinaryOp\BitwiseAnd
|| $expr instanceof Node\Expr\BinaryOp\BitwiseOr
|| $expr instanceof Node\Expr\BinaryOp\BitwiseXor
|| $expr instanceof Node\Expr\BinaryOp\Mod) {
return $this->isSafeSsaIntExpr($expr->left)
&& $this->isSafeSsaIntExpr($expr->right);
}
return false;
}
/**
* Check whether a TYPE_INT expression could overflow int64 at runtime
* and produce a float in PHP.
*/
protected function exprCanOverflowInt(NodeAbstract $expr): bool
{
// Division always produces float in PHP when operands are int
if ($expr instanceof Node\Expr\BinaryOp\Div) {
return true;
}
if ($expr instanceof Node\Expr\BinaryOp\Plus
|| $expr instanceof Node\Expr\BinaryOp\Minus
|| $expr instanceof Node\Expr\BinaryOp\Mul) {
return true;
}
// ** can easily overflow (e.g. 2**63 already exceeds INT64_MAX)
if ($expr instanceof Node\Expr\BinaryOp\Pow) {
return true;
}
if ($expr instanceof Node\Expr\FuncCall
&& $expr->name instanceof Node\Name
&& strtolower($expr->name->toString()) === 'pow') {
return true;
}
if ($expr instanceof Node\Expr\BinaryOp) {
return $this->exprCanOverflowInt($expr->left)
|| $this->exprCanOverflowInt($expr->right);
}
return false;
}
protected function isBoundaryConstant(NodeAbstract $node): bool
{
if ($node instanceof Node\Expr\ConstFetch
&& $node->name instanceof Node\Name) {
$name = $node->name->toLowerString();
return $name === 'php_int_max' || $name === 'php_int_min';
}
return false;
}
protected function phiSourcesHaveMixedTypes(SsaBuilder $ssa, array $varList, string $narrowedType): bool
{
foreach ($varList as $ssaVar) {
if (($ssaVar->flags & SsaFlags::PHI) && !empty($ssaVar->phiSources)) {
foreach ($ssaVar->phiSources as $srcSsaId) {
if (isset($ssa->ssaVars[$srcSsaId])) {
$srcVar = $ssa->ssaVars[$srcSsaId];
$srcType = $this->detectSsaDefType($srcVar);
if ($srcType !== null && $srcType !== $narrowedType) {
return true;
}
}
}
}
}
return false;
}
/**
* Check whether $varName is used in any operation that would produce
* incorrect results with a narrowed C++ int64_t. Only bitwise ops
* (& | ^ << >> ~) and modulo (%) are safe — arithmetic can overflow
* to float, which int64_t would wrap instead.
*/
protected function hasDangerousIntOps(string $varName, array $stmts): bool
{
foreach ($stmts as $stmt) {
if ($this->scanStmtForDangerousIntOps($stmt, $varName)) {
return true;
}
}
return false;
}
protected function scanStmtForDangerousIntOps($stmt, string $varName): bool
{
if (!$stmt instanceof Node) {
return false;
}
// Check conditions in control-flow statements
if (($stmt instanceof Node\Stmt\If_
|| $stmt instanceof Node\Stmt\While_
|| $stmt instanceof Node\Stmt\Do_)
&& $stmt->cond instanceof Node
&& $this->exprHasIntHazard($stmt->cond, $varName)) {
return true;
}
if ($stmt instanceof Node\Stmt\For_) {
foreach ($stmt->cond as $cond) {
if ($cond instanceof Node && $this->exprHasIntHazard($cond, $varName)) {
return true;
}
}
foreach ($stmt->init as $init) {
if ($init instanceof Node && $this->exprHasIntHazard($init, $varName)) {
return true;
}
}
foreach ($stmt->loop as $loop) {
if ($loop instanceof Node && $this->exprHasIntHazard($loop, $varName)) {
return true;
}
}
}
if ($stmt instanceof Node\Stmt\Foreach_) {
if ($this->exprHasIntHazard($stmt->expr, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Switch_) {
if ($this->exprHasIntHazard($stmt->cond, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Expression) {
if ($this->exprHasIntHazard($stmt->expr, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr instanceof Node) {
if ($this->exprHasIntHazard($stmt->expr, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Echo_) {
foreach ($stmt->exprs as $expr) {
if ($expr instanceof Node && $this->exprHasIntHazard($expr, $varName)) {
return true;
}
}
}
return $this->recurseForDangerousOps($stmt, $varName, 'Int');
}
/**
* Recursively check an expression for hazardous int usage.
*
* Only bitwise ops (& | ^ << >> ~) and modulo (%) are safe for a
* narrowed int64_t. Any other BinaryOp, AssignOp, UnaryMinus, or
* Inc/Dec that involves $varName is a hazard.
*/
protected function exprHasIntHazard($expr, string $varName): bool
{
if (!$expr instanceof Node) {
return false;
}
$type = $expr->getType();
// Safe binary ops: bitwise + modulo — recurse into children
if (isset(self::SAFE_INT_BINARY_OPS[$type])) {
return $this->exprHasIntHazard($expr->left, $varName)
|| $this->exprHasIntHazard($expr->right, $varName);
}
// Unsafe binary op: flag if $varName appears anywhere inside
if ($expr instanceof Node\Expr\BinaryOp) {
if ($this->exprUsesVar($expr, $varName)) {
return true;
}
return $this->exprHasIntHazard($expr->left, $varName)
|| $this->exprHasIntHazard($expr->right, $varName);
}
// Compound assignment can change the variable type or depends on
// PHP's integer conversion edge cases. Treat all of them as hazards
// for int narrowing.
if ($expr instanceof Node\Expr\AssignOp) {
if ($this->isVarNamed($expr->var, $varName)) {
return true;
}
return $this->exprHasIntHazard($expr->expr, $varName);
}
// Assignment: $var = rhs — LHS is a definition, safe; check RHS
if ($expr instanceof Node\Expr\Assign) {
return $this->exprHasIntHazard($expr->expr, $varName);
}
// ~$varName is always int, safe
if ($expr instanceof Node\Expr\BitwiseNot) {
return $this->exprHasIntHazard($expr->expr, $varName);
}
// -$varName can overflow INT_MIN → float
if ($expr instanceof Node\Expr\UnaryMinus) {
if ($this->exprUsesVar($expr, $varName)) {
return true;
}
return false;
}
// $varName++, ++$varName, $varName--, --$varName can overflow
if ($expr instanceof Node\Expr\PreInc || $expr instanceof Node\Expr\PreDec
|| $expr instanceof Node\Expr\PostInc || $expr instanceof Node\Expr\PostDec) {
return $this->isVarNamed($expr->var, $varName);
}
// (int)$varName — safe type read
if ($expr instanceof Node\Expr\Cast\Int_) {
return $this->exprHasIntHazard($expr->expr, $varName);
}
// Generic recursion over common expression sub-nodes
foreach (['left', 'right', 'expr', 'var', 'cond', 'if', 'else', 'dim', 'value'] as $prop) {
if (isset($expr->$prop) && $expr->$prop instanceof Node) {
if ($this->exprHasIntHazard($expr->$prop, $varName)) {
return true;
}
}
}
foreach (['args', 'exprs', 'items', 'stmts'] as $prop) {
if (isset($expr->$prop) && is_array($expr->$prop)) {
foreach ($expr->$prop as $item) {
if ($item instanceof Node) {
if ($this->exprHasIntHazard($item, $varName)) {
return true;
}
}
}
}
}
return false;
}
/**
* Check whether $varName appears anywhere recursively in an AST node.
*/
protected function exprUsesVar($node, string $varName): bool
{
if (!$node instanceof Node) {
return false;
}
if ($this->isVarNamed($node, $varName)) {
return true;
}
foreach (['left', 'right', 'expr', 'var', 'cond', 'if', 'else', 'dim', 'value'] as $prop) {
if (isset($node->$prop) && $node->$prop instanceof Node) {
if ($this->exprUsesVar($node->$prop, $varName)) {
return true;
}
}
}
foreach (['args', 'exprs', 'items', 'stmts'] as $prop) {
if (isset($node->$prop) && is_array($node->$prop)) {
foreach ($node->$prop as $item) {
if ($item instanceof Node) {
if ($this->exprUsesVar($item, $varName)) {
return true;
}
}
}
}
}
return false;
}
protected function isVarNamed($node, string $varName): bool
{
return $node instanceof Node\Expr\Variable
&& is_string($node->name)
&& ($node->name === $varName || $this->escapeVarName($node->name) === $varName);
}
protected function hasDangerousFloatOps(string $varName, array $stmts): bool
{
foreach ($stmts as $stmt) {
if ($this->scanStmtForDangerousFloatOps($stmt, $varName)) {
return true;
}
}
return false;
}
protected function scanStmtForDangerousFloatOps($stmt, string $varName): bool
{
if (!$stmt instanceof Node) {
return false;
}
if ($stmt instanceof Node\Stmt\Expression && $this->exprHasFloatHazard($stmt->expr, $varName)) {
return true;
}
if (($stmt instanceof Node\Stmt\If_
|| $stmt instanceof Node\Stmt\While_
|| $stmt instanceof Node\Stmt\Do_)
&& $stmt->cond instanceof Node
&& $this->exprHasFloatHazard($stmt->cond, $varName)) {
return true;
}
if ($stmt instanceof Node\Stmt\For_) {
foreach ([$stmt->init, $stmt->cond, $stmt->loop] as $exprList) {
foreach ($exprList as $expr) {
if ($expr instanceof Node && $this->exprHasFloatHazard($expr, $varName)) {
return true;
}
}
}
}
if ($stmt instanceof Node\Stmt\Foreach_) {
if ($this->exprHasFloatHazard($stmt->expr, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Switch_) {
if ($this->exprHasFloatHazard($stmt->cond, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr instanceof Node) {
if ($this->exprHasFloatHazard($stmt->expr, $varName)) {
return true;
}
}
if ($stmt instanceof Node\Stmt\Echo_) {
foreach ($stmt->exprs as $expr) {
if ($expr instanceof Node && $this->exprHasFloatHazard($expr, $varName)) {
return true;
}
}
}
return $this->recurseForDangerousOps($stmt, $varName, 'Float');
}
protected function exprHasFloatHazard($expr, string $varName): bool
{
if (!$expr instanceof Node) {
return false;
}
if (isset(self::FLOAT_INT_ONLY_OPS[$expr->getType()]) && $this->exprUsesVar($expr, $varName)) {
return true;
}
foreach (['left', 'right', 'expr', 'var', 'cond', 'if', 'else', 'dim', 'value'] as $prop) {
if (isset($expr->$prop) && $expr->$prop instanceof Node) {
if ($this->exprHasFloatHazard($expr->$prop, $varName)) {
return true;
}
}
}
foreach (['args', 'exprs', 'items', 'stmts'] as $prop) {
if (isset($expr->$prop) && is_array($expr->$prop)) {
foreach ($expr->$prop as $item) {
if ($item instanceof Node && $this->exprHasFloatHazard($item, $varName)) {
return true;
}
}
}
}
return false;
}
/**
* Recurse into compound statements (if/else, foreach, while, for, try/catch, switch).
*/
protected function recurseForDangerousOps($stmt, string $varName, string $mode): bool
{
$method = 'hasDangerous' . $mode . 'Ops';
if ($stmt instanceof Node\Stmt\If_) {
if ($this->$method($varName, $stmt->stmts)) return true;
if (!empty($stmt->elseifs)) {
foreach ($stmt->elseifs as $elseif) {
if ($this->$method($varName, $elseif->stmts)) return true;
}
}
if ($stmt->else && $this->$method($varName, $stmt->else->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\Foreach_) {
if ($this->$method($varName, $stmt->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\While_ || $stmt instanceof Node\Stmt\Do_) {
if ($this->$method($varName, $stmt->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\For_) {
if ($this->$method($varName, $stmt->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\TryCatch) {
if ($this->$method($varName, $stmt->stmts)) return true;
foreach ($stmt->catches as $catch) {
if ($this->$method($varName, $catch->stmts)) return true;
}
if ($stmt->finally && $this->$method($varName, $stmt->finally->stmts)) return true;
}
if ($stmt instanceof Node\Stmt\Case_ || $stmt instanceof Node\Stmt\Switch_) {
if (isset($stmt->stmts) && $this->$method($varName, $stmt->stmts)) return true;
}
return false;
}
}