|
|
|
|
@ -17,6 +17,31 @@ use PhpParser\NodeAbstract; |
|
|
|
|
|
|
|
|
|
trait SsaTypeOptimizer |
|
|
|
|
{ |
|
|
|
|
/** |
|
|
|
|
* Binary ops that always produce int from int operands and never overflow |
|
|
|
|
* to float. Bitwise operations and modulo are safe; arithmetic is not. |
|
|
|
|
*/ |
|
|
|
|
private const 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, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Compound assignment ops that keep the variable int-typed. |
|
|
|
|
*/ |
|
|
|
|
private const SAFE_INT_ASSIGN_OPS = [ |
|
|
|
|
'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. |
|
|
|
|
* |
|
|
|
|
@ -181,15 +206,19 @@ trait SsaTypeOptimizer |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Check whether a TYPE_INT expression involves +, -, *, ** operations |
|
|
|
|
* that could overflow int64 at runtime and produce a float. |
|
|
|
|
* Check whether a TYPE_INT expression could overflow int64 at runtime |
|
|
|
|
* and produce a float in PHP. |
|
|
|
|
*/ |
|
|
|
|
private 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 |
|
|
|
|
|| $expr instanceof Node\Expr\BinaryOp\Pow) { |
|
|
|
|
|| $expr instanceof Node\Expr\BinaryOp\Mul) { |
|
|
|
|
$leftConst = $this->getIntConstantValue($expr->left); |
|
|
|
|
$rightConst = $this->getIntConstantValue($expr->right); |
|
|
|
|
|
|
|
|
|
@ -198,7 +227,6 @@ trait SsaTypeOptimizer |
|
|
|
|
$expr instanceof Node\Expr\BinaryOp\Plus => $leftConst + $rightConst, |
|
|
|
|
$expr instanceof Node\Expr\BinaryOp\Minus => $leftConst - $rightConst, |
|
|
|
|
$expr instanceof Node\Expr\BinaryOp\Mul => $leftConst * $rightConst, |
|
|
|
|
$expr instanceof Node\Expr\BinaryOp\Pow => $leftConst ** $rightConst, |
|
|
|
|
}; |
|
|
|
|
return $result > PHP_INT_MAX || $result < PHP_INT_MIN; |
|
|
|
|
} |
|
|
|
|
@ -211,6 +239,11 @@ trait SsaTypeOptimizer |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ** 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') { |
|
|
|
|
@ -261,6 +294,12 @@ trait SsaTypeOptimizer |
|
|
|
|
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. |
|
|
|
|
*/ |
|
|
|
|
private function hasDangerousIntOps(string $varName, array $stmts): bool |
|
|
|
|
{ |
|
|
|
|
foreach ($stmts as $stmt) { |
|
|
|
|
@ -277,36 +316,207 @@ trait SsaTypeOptimizer |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\AssignOp) { |
|
|
|
|
$lhs = $stmt->expr->var; |
|
|
|
|
if ($lhs instanceof Expr\Variable && is_string($lhs->name) && $lhs->name === $varName) { |
|
|
|
|
if ($stmt->expr instanceof Expr\AssignOp\Div) { |
|
|
|
|
// 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->expr instanceof Expr\AssignOp\Pow) { |
|
|
|
|
|
|
|
|
|
if ($stmt instanceof Node\Stmt\For_) { |
|
|
|
|
foreach ($stmt->cond as $cond) { |
|
|
|
|
if ($cond instanceof Node && $this->exprHasIntHazard($cond, $varName)) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if ($stmt->expr instanceof Expr\AssignOp\Plus |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\Minus |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\Mul |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\Mod |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\Concat |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\ShiftLeft |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\ShiftRight |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\BitwiseAnd |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\BitwiseOr |
|
|
|
|
|| $stmt->expr instanceof Expr\AssignOp\BitwiseXor) { |
|
|
|
|
$rhsType = $this->detectTypeOfExpr($stmt->expr->expr); |
|
|
|
|
if ($rhsType !== self::TYPE_INT) { |
|
|
|
|
} |
|
|
|
|
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_ && $stmt->expr instanceof Node) { |
|
|
|
|
if ($this->exprHasIntHazard($stmt->expr, $varName)) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ($stmt instanceof Node\Stmt\Switch_ && $stmt->cond instanceof Node) { |
|
|
|
|
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. |
|
|
|
|
*/ |
|
|
|
|
private 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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Safe compound assignment: $varName &= ..., etc. — recurse into RHS |
|
|
|
|
if (isset(self::SAFE_INT_ASSIGN_OPS[$type])) { |
|
|
|
|
return $this->exprHasIntHazard($expr->expr, $varName); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Unsafe compound assignment: $varName += ..., etc. |
|
|
|
|
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. |
|
|
|
|
*/ |
|
|
|
|
private 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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private function isVarNamed($node, string $varName): bool |
|
|
|
|
{ |
|
|
|
|
return $node instanceof Node\Expr\Variable |
|
|
|
|
&& is_string($node->name) |
|
|
|
|
&& $node->name === $varName; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private function hasDangerousFloatOps(string $varName, array $stmts): bool |
|
|
|
|
{ |
|
|
|
|
foreach ($stmts as $stmt) { |
|
|
|
|
|