refactor(php): 优化PHP编译器中的整数类型推断和溢出检测

- 启用之前被注释掉的 optimizeVarTypes() 方法调用
- 添加安全整数运算操作的常量定义,包括位运算和取模运算
- 改进表达式溢出检测逻辑,移除幂运算的特殊处理并将其标记为危险操作
- 实现更精确的整数运算危害检查函数 exprHasIntHazard
- 添加递归变量使用检测功能 exprUsesVar 和 isVarNamed 辅助方法
- 扩展危险整数操作检查范围,涵盖控制流语句中的条件表达式
-
pull/1/head
韩天峰 3 months ago
parent 571efa94c4
commit 2af89f270e
  1. 3
      src/Php/CompilerBase.php
  2. 258
      src/Php/Optimizer/SsaTypeOptimizer.php

@ -1158,8 +1158,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->context->ssaBuilder = new SsaBuilder($v->stmts, $this->functionDef->argInfoList);
$this->context->ssaBuilder->build();
// Narrow local variable types based on SSA analysis
// TODO: 优化变量类型,不可靠,静态分析几乎无法判断是否会溢出
// $this->optimizeVarTypes();
$this->optimizeVarTypes();
// Analyze object stability for property reference hoisting
$this->optimizeObjectProps();
}

@ -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,29 +316,61 @@ 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 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\Pow) {
}
foreach ($stmt->init as $init) {
if ($init instanceof Node && $this->exprHasIntHazard($init, $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) {
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;
}
}
}
@ -307,6 +378,145 @@ trait SsaTypeOptimizer
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) {

Loading…
Cancel
Save