diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index bd62fd4e..e1862ddf 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.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(); } diff --git a/src/Php/Optimizer/SsaTypeOptimizer.php b/src/Php/Optimizer/SsaTypeOptimizer.php index 2cbd3f33..baf9893f 100644 --- a/src/Php/Optimizer/SsaTypeOptimizer.php +++ b/src/Php/Optimizer/SsaTypeOptimizer.php @@ -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) {