From fd22ea34d9c8551cf2780dc8acaa469f6b306047 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sat, 11 Jul 2026 15:56:24 +0800 Subject: [PATCH] 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 --- src/CompilerBase.php | 221 +-------------------- src/Parser/ExceptionControlFlowTrait.php | 238 +++++++++++++++++++++++ 2 files changed, 240 insertions(+), 219 deletions(-) create mode 100644 src/Parser/ExceptionControlFlowTrait.php diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 08e7530a..4ac1d069 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.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 = []; diff --git a/src/Parser/ExceptionControlFlowTrait.php b/src/Parser/ExceptionControlFlowTrait.php new file mode 100644 index 00000000..76224229 --- /dev/null +++ b/src/Parser/ExceptionControlFlowTrait.php @@ -0,0 +1,238 @@ +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; + } + +} +