refactor(compiler): move exception control flow logic to separate trait

- Extract throw and try/catch/finally parsing methods to ExceptionControlFlowTrait
- Add ExceptionControlFlowTrait import to CompilerBase class
- Remove duplicate method implementations from CompilerBase
- Move all exception handling related functionality to dedicated trait file
- Maintain same functionality while improving
pull/17/head
韩天峰 2 months ago
parent a9e93eda28
commit fd22ea34d9
  1. 221
      src/CompilerBase.php
  2. 238
      src/Parser/ExceptionControlFlowTrait.php

@ -37,6 +37,7 @@ use TypePhp\Optimizer\LoopVarOptimizer;
use TypePhp\Parser\StdContainerTrait;
use TypePhp\Parser\AssignOpTrait;
use TypePhp\Parser\BinaryOpTrait;
use TypePhp\Parser\ExceptionControlFlowTrait;
use TypePhp\Parser\FunctionCallTrait;
use TypePhp\Parser\TypeConversionTrait;
use TypePhp\Parser\TypeDetectionTrait;
@ -88,6 +89,7 @@ class CompilerBase implements PropertyAccessContext
use MagicMethodDetector;
use StdContainerTrait;
use BinaryOpTrait;
use ExceptionControlFlowTrait;
use FunctionCallTrait;
use TypeConversionTrait;
use TypeDetectionTrait;
@ -6613,225 +6615,6 @@ class CompilerBase implements PropertyAccessContext
return $tmpVar;
}
protected function parseThrow(mixed $expr): string
{
if ($this->method === '__destruct') {
$this->warning($expr, "Throwing exception in {$this->getFullClassName()}::__destruct() may cause memory leak");
}
$type = $this->detectTypeOfExpr($expr->expr);
if ($this->isNewExpr($expr->expr)) {
$ex = $this->parseExpr($expr->expr);
return 'php::throwException(' . $ex . ')';
} elseif ($this->isVarExpr($expr->expr)) {
$ex = $this->parseIdentifier($expr->expr);
if ($type == self::TYPE_OBJECT) {
return 'php::throwException(' . $ex . ')';
}
} else {
$ex = $this->parseExpr($expr->expr);
}
if ($type != self::TYPE_VAR) {
$this->fatalError($expr, 'Can only throw objects');
}
return 'php::throwValue(' . $ex . ')';
}
protected function parseTryCatch(mixed $v): string
{
$code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'try {';
$finally = $v->finally;
$stmts = $finally ? $this->injectFinallyBeforeReturn($v->stmts, $finally->stmts) : $v->stmts;
$catches = $v->catches;
// Pre-register catch variables before parsing the try block so that
// injected finally code referencing catch variables (e.g. $e) does not
// trigger undefined-variable errors.
if ($finally) {
foreach ($catches as $catch) {
if ($catch->var) {
$varName = $this->parseIdentifier($catch->var);
if (!$this->hasVar($varName) && $this->stmtListUsesVariable($finally->stmts, $varName)) {
$this->addLocalVar($varName, self::TYPE_OBJECT);
}
}
}
}
$code .= PHP_EOL;
$code .= $this->parseBlockStmts($stmts);
$code .= $this->getIndent() . '}' . PHP_EOL;
$exVar = $this->genTmpVarName();
$this->addLocalVar($exVar, self::TYPE_VAR);
$code .= 'catch(zend_object *_ex) {' . PHP_EOL;
$code .= $this->getIndent() . $exVar . ' = php::catchException();' . PHP_EOL;
if ($catches) {
$catchMatched = $this->genTmpVarName();
$code .= $this->getIndent() . 'bool ' . $catchMatched . ' = false;' . PHP_EOL;
$this->indentLevel++;
foreach ($catches as $catch) {
$code .= $this->parseCatch($catch, $exVar, $catchMatched, $finally?->stmts ?? []);
}
$this->indentLevel--;
}
$code .= '}' . PHP_EOL;
if ($finally) {
$code .= $this->parseStmts($finally->stmts);
$code .= PHP_EOL;
}
$rethrow = $this->inGeneratorBody
? 'typephp_fiber_rethrow(' . $exVar . ');'
: 'php::throwException(php::Object(' . $exVar . '));';
$code .= 'if (' . $exVar . ') {' . PHP_EOL . $this->getIndent() . $rethrow . PHP_EOL . $this->getIndent() . '}';
return $code;
}
protected function injectFinallyBeforeReturn(array $stmts, array $finallyStmts, int $localControlDepth = 0): array
{
$result = [];
foreach ($stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Return_) {
if ($stmt->expr) {
$tmpVar = $this->addTmpVar(self::TYPE_VAR);
$result[] = new Node\Stmt\Expression(new Expr\Assign(new Variable($tmpVar), $stmt->expr));
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = new Node\Stmt\Return_(new Variable($tmpVar));
continue;
}
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = $stmt;
continue;
}
if ($stmt instanceof Node\Stmt\Break_ || $stmt instanceof Node\Stmt\Continue_) {
$level = $stmt->num instanceof Node\Scalar\Int_ ? $stmt->num->value : 1;
if ($level > $localControlDepth) {
array_push($result, ...$this->cloneStmtList($finallyStmts));
}
$result[] = $stmt;
continue;
}
if ($stmt instanceof Node\Stmt\Goto_) {
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = $stmt;
continue;
}
$result[] = $this->injectFinallyBeforeReturnInStmt($stmt, $finallyStmts, $localControlDepth);
}
return $result;
}
protected function injectFinallyBeforeReturnInStmt(Node\Stmt $stmt, array $finallyStmts, int $localControlDepth): Node\Stmt
{
if ($stmt instanceof Node\Stmt\If_) {
$stmt = clone $stmt;
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth);
foreach ($stmt->elseifs as $index => $elseIf) {
$elseIf = clone $elseIf;
$elseIf->stmts = $this->injectFinallyBeforeReturn($elseIf->stmts, $finallyStmts, $localControlDepth);
$stmt->elseifs[$index] = $elseIf;
}
if ($stmt->else) {
$stmt->else = clone $stmt->else;
$stmt->else->stmts = $this->injectFinallyBeforeReturn($stmt->else->stmts, $finallyStmts, $localControlDepth);
}
return $stmt;
}
if ($stmt instanceof Node\Stmt\For_
|| $stmt instanceof Node\Stmt\Foreach_
|| $stmt instanceof Node\Stmt\While_
|| $stmt instanceof Node\Stmt\Do_
) {
$stmt = clone $stmt;
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth + 1);
return $stmt;
}
if ($stmt instanceof Node\Stmt\Switch_) {
$stmt = clone $stmt;
foreach ($stmt->cases as $index => $case) {
$case = clone $case;
$case->stmts = $this->injectFinallyBeforeReturn($case->stmts, $finallyStmts, $localControlDepth + 1);
$stmt->cases[$index] = $case;
}
return $stmt;
}
return $stmt;
}
protected function cloneStmtList(array $stmts): array
{
return array_map(static fn (Node\Stmt $stmt): Node\Stmt => clone $stmt, $stmts);
}
protected function stmtListUsesVariable(array $stmts, string $name): bool
{
$nodeFinder = new NodeFinder();
foreach ($nodeFinder->findInstanceOf($stmts, Variable::class) as $var) {
if (is_string($var->name) && $this->escapeVarName($var->name) === $name) {
return true;
}
}
return false;
}
protected function parseCatch(Node\Stmt\Catch_ $catch, string $exVar, string $catchMatched, array $finallyStmts = []): string
{
$types = $catch->types;
$var = $catch->var ? $this->parseIdentifier($catch->var) : '';
if ($var !== '' && !$this->hasVar($var)) {
$this->addLocalVar($var, self::TYPE_OBJECT);
}
$code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= $this->getIndent() . 'if (!' . $catchMatched . ' && ' . $exVar . ' && ';
$conditions = [];
foreach ($types as $type) {
if ($this->isNameExpr($type) or $this->isFullNameExpr($type)) {
$class = $this->getNamespacedClassName($this->parseIdentifier($type));
$ce = $this->getClassEntryPtr($class);
$conditions[] = Symbol::instanceOf() . '(' . $exVar . ', ' . $ce . ')';
} else {
$this->fatalError($type, 'Unsupported catch type');
}
}
$code .= '(' . implode(' || ', $conditions) . ')) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . $catchMatched . ' = true;' . PHP_EOL;
if ($var !== '') {
$code .= $this->getIndent() . $var . ' = ' . $exVar . ';' . PHP_EOL;
}
$code .= $this->getIndent() . "{$exVar} = php::null;" . PHP_EOL;
$stmts = $finallyStmts ? $this->injectFinallyBeforeReturn($catch->stmts, $finallyStmts) : $catch->stmts;
if ($finallyStmts) {
$code .= $this->getIndent() . 'try {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->parseStmts($stmts);
$this->indentLevel--;
$code .= $this->getIndent() . '} catch(zend_object *_catch_throw_ex) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . "{$exVar} = php::catchException();" . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
} else {
$code .= $this->parseStmts($stmts);
}
$this->indentLevel--;
$code .= $this->getIndent() . '}';
return $code;
}
protected function parseShellExec(Expr\ShellExec $expr): string
{
$list = [];

@ -0,0 +1,238 @@
<?php
/**
* This file is part of TypePHP.
*
* Lowers throw and try/catch/finally control flow.
*/
namespace TypePhp\Parser;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Variable;
use PhpParser\NodeFinder;
use TypePhp\Symbol;
trait ExceptionControlFlowTrait
{
protected function parseThrow(mixed $expr): string
{
if ($this->method === '__destruct') {
$this->warning($expr, "Throwing exception in {$this->getFullClassName()}::__destruct() may cause memory leak");
}
$type = $this->detectTypeOfExpr($expr->expr);
if ($this->isNewExpr($expr->expr)) {
$ex = $this->parseExpr($expr->expr);
return 'php::throwException(' . $ex . ')';
} elseif ($this->isVarExpr($expr->expr)) {
$ex = $this->parseIdentifier($expr->expr);
if ($type == self::TYPE_OBJECT) {
return 'php::throwException(' . $ex . ')';
}
} else {
$ex = $this->parseExpr($expr->expr);
}
if ($type != self::TYPE_VAR) {
$this->fatalError($expr, 'Can only throw objects');
}
return 'php::throwValue(' . $ex . ')';
}
protected function parseTryCatch(mixed $v): string
{
$code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= 'try {';
$finally = $v->finally;
$stmts = $finally ? $this->injectFinallyBeforeReturn($v->stmts, $finally->stmts) : $v->stmts;
$catches = $v->catches;
// Pre-register catch variables before parsing the try block so that
// injected finally code referencing catch variables (e.g. $e) does not
// trigger undefined-variable errors.
if ($finally) {
foreach ($catches as $catch) {
if ($catch->var) {
$varName = $this->parseIdentifier($catch->var);
if (!$this->hasVar($varName) && $this->stmtListUsesVariable($finally->stmts, $varName)) {
$this->addLocalVar($varName, self::TYPE_OBJECT);
}
}
}
}
$code .= PHP_EOL;
$code .= $this->parseBlockStmts($stmts);
$code .= $this->getIndent() . '}' . PHP_EOL;
$exVar = $this->genTmpVarName();
$this->addLocalVar($exVar, self::TYPE_VAR);
$code .= 'catch(zend_object *_ex) {' . PHP_EOL;
$code .= $this->getIndent() . $exVar . ' = php::catchException();' . PHP_EOL;
if ($catches) {
$catchMatched = $this->genTmpVarName();
$code .= $this->getIndent() . 'bool ' . $catchMatched . ' = false;' . PHP_EOL;
$this->indentLevel++;
foreach ($catches as $catch) {
$code .= $this->parseCatch($catch, $exVar, $catchMatched, $finally?->stmts ?? []);
}
$this->indentLevel--;
}
$code .= '}' . PHP_EOL;
if ($finally) {
$code .= $this->parseStmts($finally->stmts);
$code .= PHP_EOL;
}
$rethrow = $this->inGeneratorBody
? 'typephp_fiber_rethrow(' . $exVar . ');'
: 'php::throwException(php::Object(' . $exVar . '));';
$code .= 'if (' . $exVar . ') {' . PHP_EOL . $this->getIndent() . $rethrow . PHP_EOL . $this->getIndent() . '}';
return $code;
}
protected function injectFinallyBeforeReturn(array $stmts, array $finallyStmts, int $localControlDepth = 0): array
{
$result = [];
foreach ($stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Return_) {
if ($stmt->expr) {
$tmpVar = $this->addTmpVar(self::TYPE_VAR);
$result[] = new Node\Stmt\Expression(new Expr\Assign(new Variable($tmpVar), $stmt->expr));
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = new Node\Stmt\Return_(new Variable($tmpVar));
continue;
}
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = $stmt;
continue;
}
if ($stmt instanceof Node\Stmt\Break_ || $stmt instanceof Node\Stmt\Continue_) {
$level = $stmt->num instanceof Node\Scalar\Int_ ? $stmt->num->value : 1;
if ($level > $localControlDepth) {
array_push($result, ...$this->cloneStmtList($finallyStmts));
}
$result[] = $stmt;
continue;
}
if ($stmt instanceof Node\Stmt\Goto_) {
array_push($result, ...$this->cloneStmtList($finallyStmts));
$result[] = $stmt;
continue;
}
$result[] = $this->injectFinallyBeforeReturnInStmt($stmt, $finallyStmts, $localControlDepth);
}
return $result;
}
protected function injectFinallyBeforeReturnInStmt(Node\Stmt $stmt, array $finallyStmts, int $localControlDepth): Node\Stmt
{
if ($stmt instanceof Node\Stmt\If_) {
$stmt = clone $stmt;
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth);
foreach ($stmt->elseifs as $index => $elseIf) {
$elseIf = clone $elseIf;
$elseIf->stmts = $this->injectFinallyBeforeReturn($elseIf->stmts, $finallyStmts, $localControlDepth);
$stmt->elseifs[$index] = $elseIf;
}
if ($stmt->else) {
$stmt->else = clone $stmt->else;
$stmt->else->stmts = $this->injectFinallyBeforeReturn($stmt->else->stmts, $finallyStmts, $localControlDepth);
}
return $stmt;
}
if ($stmt instanceof Node\Stmt\For_
|| $stmt instanceof Node\Stmt\Foreach_
|| $stmt instanceof Node\Stmt\While_
|| $stmt instanceof Node\Stmt\Do_
) {
$stmt = clone $stmt;
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth + 1);
return $stmt;
}
if ($stmt instanceof Node\Stmt\Switch_) {
$stmt = clone $stmt;
foreach ($stmt->cases as $index => $case) {
$case = clone $case;
$case->stmts = $this->injectFinallyBeforeReturn($case->stmts, $finallyStmts, $localControlDepth + 1);
$stmt->cases[$index] = $case;
}
return $stmt;
}
return $stmt;
}
protected function cloneStmtList(array $stmts): array
{
return array_map(static fn (Node\Stmt $stmt): Node\Stmt => clone $stmt, $stmts);
}
protected function stmtListUsesVariable(array $stmts, string $name): bool
{
$nodeFinder = new NodeFinder();
foreach ($nodeFinder->findInstanceOf($stmts, Variable::class) as $var) {
if (is_string($var->name) && $this->escapeVarName($var->name) === $name) {
return true;
}
}
return false;
}
protected function parseCatch(Node\Stmt\Catch_ $catch, string $exVar, string $catchMatched, array $finallyStmts = []): string
{
$types = $catch->types;
$var = $catch->var ? $this->parseIdentifier($catch->var) : '';
if ($var !== '' && !$this->hasVar($var)) {
$this->addLocalVar($var, self::TYPE_OBJECT);
}
$code = $this->parseBeforeStmtLines() . PHP_EOL;
$code .= $this->getIndent() . 'if (!' . $catchMatched . ' && ' . $exVar . ' && ';
$conditions = [];
foreach ($types as $type) {
if ($this->isNameExpr($type) or $this->isFullNameExpr($type)) {
$class = $this->getNamespacedClassName($this->parseIdentifier($type));
$ce = $this->getClassEntryPtr($class);
$conditions[] = Symbol::instanceOf() . '(' . $exVar . ', ' . $ce . ')';
} else {
$this->fatalError($type, 'Unsupported catch type');
}
}
$code .= '(' . implode(' || ', $conditions) . ')) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . $catchMatched . ' = true;' . PHP_EOL;
if ($var !== '') {
$code .= $this->getIndent() . $var . ' = ' . $exVar . ';' . PHP_EOL;
}
$code .= $this->getIndent() . "{$exVar} = php::null;" . PHP_EOL;
$stmts = $finallyStmts ? $this->injectFinallyBeforeReturn($catch->stmts, $finallyStmts) : $catch->stmts;
if ($finallyStmts) {
$code .= $this->getIndent() . 'try {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->parseStmts($stmts);
$this->indentLevel--;
$code .= $this->getIndent() . '} catch(zend_object *_catch_throw_ex) {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->getIndent() . "{$exVar} = php::catchException();" . PHP_EOL;
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
} else {
$code .= $this->parseStmts($stmts);
}
$this->indentLevel--;
$code .= $this->getIndent() . '}';
return $code;
}
}
Loading…
Cancel
Save