fix: 修复解构键提取、switch默认分支及finally控制流

正确处理数组/foreach解构中的键名与嵌套结构,
修复switch中default位于匹配case前时执行顺序错误的问题,
确保finally块在break/continue/goto离开try/catch时被注入,
并支持动态静态方法与属性调用。
pull/13/head
韩天峰 2 months ago
parent bd1a4db9d4
commit 7937f05282
  1. 85
      src/Php/CompilerBase.php
  2. 5
      src/Php/Parser/AssignOpTrait.php
  3. 26
      tests/aot/array/destructure-keyed-write-targets.phpt
  4. 19
      tests/aot/array/destructure-keyed.phpt
  5. 24
      tests/aot/array/foreach-list-nested-keyed.phpt
  6. 31
      tests/aot/callable/is-callable-array.phpt
  7. 31
      tests/aot/exception/finally-break-continue.phpt
  8. 27
      tests/aot/exception/finally-catch-break.phpt
  9. 28
      tests/aot/exception/finally-catch-goto.phpt
  10. 24
      tests/aot/exception/finally-goto.phpt
  11. 24
      tests/aot/exception/finally-nested-loop-break.phpt
  12. 24
      tests/aot/loop/iterator-list-nested-keyed.phpt
  13. 35
      tests/aot/static/static-call-dynamic-method.phpt
  14. 29
      tests/aot/static/static-property-dynamic-name.phpt
  15. 24
      tests/aot/switch/default-before-matching-case.phpt

@ -5141,6 +5141,14 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
continue;
}
if ($item instanceof ArrayItem) {
$key = $item->key ? $this->parseArrayKey($item->key) : (string) $k;
if ($item->value instanceof Expr\List_) {
$nestedTmpVar = $this->genTmpVarName();
$this->addLocalVar($nestedTmpVar, self::TYPE_VAR);
$code .= $this->getIndent() . ' ' . $nestedTmpVar . ' = ' . $listTmpVar . '.item(' . $key . ');' . PHP_EOL;
$code .= $this->parseForeachItemAsList($nestedTmpVar, $item->value->items);
continue;
}
$oriInAssignExpr = $this->context->inAssignExpr;
$this->context->inAssignExpr = true;
$var = $this->parseIdentifier($item->value);
@ -5148,7 +5156,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($this->isVarExpr($item->value) and !$this->hasVar($var)) {
$this->addLocalVar($var, self::TYPE_VAR);
}
$code .= $this->getIndent() . ' ' . $var . ' = ' . $listTmpVar . '.item(' . $k . ');' . PHP_EOL;
$code .= $this->getIndent() . ' ' . $var . ' = ' . $listTmpVar . '.item(' . $key . ');' . PHP_EOL;
} else {
$this->fatalError($item, 'Unsupported foreach item type');
}
@ -5319,21 +5327,24 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$code = 'do {' . PHP_EOL;
$this->indentLevel++;
$switchTarget = $this->genTmpVarName();
$switchMatched = $this->genTmpVarName();
$code .= $this->getIndent() . 'int ' . $switchTarget . ' = -1;' . PHP_EOL;
$code .= $this->getIndent() . 'bool ' . $switchMatched . ' = false;' . PHP_EOL;
$caseConds = [];
$caseGroups = [];
$hasDefault = false;
$defaultTarget = null;
foreach ($v->cases as $case) {
if (empty($case->cond)) {
$isDefault = true;
$hasDefault = true;
} else {
$isDefault = false;
$caseConds[] = $case->cond;
}
$stmts = $case->stmts;
if (empty($stmts)) {
continue;
}
$this->indentLevel++;
if (count($stmts) === 1 and $stmts[0] instanceof Node\Stmt\Block) {
$stmts = $stmts[0]->stmts;
}
@ -5345,13 +5356,20 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
) {
$this->fatalError($case, 'switch case must end with return/break/exit/throw, ' . $lastExpr->getType() . ' given');
}
$target = count($caseGroups);
if ($hasDefault) {
$defaultTarget = $target;
}
$caseGroups[] = [$caseConds, $hasDefault, $stmts];
$caseConds = [];
$hasDefault = false;
}
if ($isDefault) {
$code .= $this->getIndent() . 'if (!' . $switchMatched . ') {' . PHP_EOL;
} else {
foreach ($caseGroups as $target => [$conds]) {
if (!empty($conds)) {
$groupMatched = $this->genTmpVarName();
$code .= $this->getIndent() . 'bool ' . $groupMatched . ' = false;' . PHP_EOL;
foreach ($caseConds as $caseCond) {
foreach ($conds as $caseCond) {
$this->assertExprCanBeUsedAsValue($caseCond, 'switch case condition');
$caseBeforeStmtCount = count($this->context->beforeStmtLines);
$caseAfterStmtCount = count($this->context->afterStmtLines);
@ -5374,13 +5392,31 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
}
$code .= $this->getIndent() . 'if (' . $groupMatched . ') {' . PHP_EOL;
$code .= $this->getIndent() . $switchMatched . ' = true;' . PHP_EOL;
$caseConds = [];
$code .= $this->getIndent() . $switchTarget . ' = ' . $target . ';' . PHP_EOL;
$code .= $this->getIndent() . '}' . PHP_EOL;
}
}
if ($defaultTarget !== null) {
$code .= $this->getIndent() . 'if (!' . $switchMatched . ') {' . PHP_EOL;
$code .= $this->getIndent() . $switchTarget . ' = ' . $defaultTarget . ';' . PHP_EOL;
$code .= $this->getIndent() . '}' . PHP_EOL;
}
foreach ($caseGroups as $target => [, , $stmts]) {
$code .= $this->getIndent() . 'if (' . $switchTarget . ' == ' . $target . ') {' . PHP_EOL;
$this->indentLevel++;
$code .= $this->parseStmts($stmts);
$this->indentLevel--;
$code .= $this->getIndent() . '}' . PHP_EOL;
}
if (!empty($caseConds) || $hasDefault) {
// PHP allows a trailing label without statements; it has no code to execute.
if ($hasDefault && $defaultTarget === null) {
$code .= $this->getIndent() . 'if (!' . $switchMatched . ') {' . PHP_EOL;
$code .= $this->getIndent() . $switchTarget . ' = -1;' . PHP_EOL;
$code .= $this->getIndent() . '}' . PHP_EOL;
}
}
$code .= $this->genLoopEndFlagCheck();
$this->indentLevel--;
$code .= $this->getIndent() . '} while (0);';
@ -6476,7 +6512,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return $code;
}
protected function injectFinallyBeforeReturn(array $stmts, array $finallyStmts): array
protected function injectFinallyBeforeReturn(array $stmts, array $finallyStmts, int $localControlDepth = 0): array
{
$result = [];
foreach ($stmts as $stmt) {
@ -6493,24 +6529,39 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
continue;
}
$result[] = $this->injectFinallyBeforeReturnInStmt($stmt, $finallyStmts);
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): Node\Stmt
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);
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth);
foreach ($stmt->elseifs as $index => $elseIf) {
$elseIf = clone $elseIf;
$elseIf->stmts = $this->injectFinallyBeforeReturn($elseIf->stmts, $finallyStmts);
$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);
$stmt->else->stmts = $this->injectFinallyBeforeReturn($stmt->else->stmts, $finallyStmts, $localControlDepth);
}
return $stmt;
}
@ -6521,7 +6572,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
|| $stmt instanceof Node\Stmt\Do_
) {
$stmt = clone $stmt;
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts);
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth + 1);
return $stmt;
}
@ -6529,7 +6580,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$stmt = clone $stmt;
foreach ($stmt->cases as $index => $case) {
$case = clone $case;
$case->stmts = $this->injectFinallyBeforeReturn($case->stmts, $finallyStmts);
$case->stmts = $this->injectFinallyBeforeReturn($case->stmts, $finallyStmts, $localControlDepth + 1);
$stmt->cases[$index] = $case;
}
return $stmt;

@ -118,10 +118,11 @@ trait AssignOpTrait
continue;
}
if ($item instanceof ArrayItem) {
$key = $item->key ? $this->parseArrayKey($item->key) : (string) $k;
if ($item->value instanceof Expr\List_) {
$nestedTmp = $this->genTmpVarName();
$this->addLocalVar($nestedTmp, self::TYPE_ARRAY);
$code .= "{$nestedTmp} = {$tmpVar}.item({$k}); ";
$code .= "{$nestedTmp} = {$tmpVar}.item({$key}); ";
$code .= $this->parseAssignToList($item->value, new Variable($nestedTmp));
} else {
$oriInAssignExpr = $this->context->inAssignExpr;
@ -131,7 +132,7 @@ trait AssignOpTrait
if ($this->isVarExpr($item->value) and !$this->hasVar($var)) {
$this->addLocalVar($var, self::TYPE_VAR);
}
$code .= "{$var} = {$tmpVar}.item({$k}); ";
$code .= "{$var} = {$tmpVar}.item({$key}); ";
}
} else {
abort($item);

@ -0,0 +1,26 @@
--TEST--
keyed destructuring writes to object properties and array dimensions
--FILE--
<?php
class DestructureKeyedBox
{
public string $name = '';
}
function main(): void
{
$box = new DestructureKeyedBox();
$out = [];
['name' => $box->name, 'value' => $out['value']] = [
'name' => 'alpha',
'value' => 123,
];
var_dump($box->name, $out['value']);
}
?>
--EXPECT--
string(5) "alpha"
int(123)

@ -0,0 +1,19 @@
--TEST--
array destructuring assignment supports keyed and nested items
--FILE--
<?php
function main(): void
{
['id' => $id, 'pair' => [$left, $right]] = [
'id' => 42,
'pair' => ['left', 'right'],
];
var_dump($id, $left, $right);
}
?>
--EXPECT--
int(42)
string(4) "left"
string(5) "right"

@ -0,0 +1,24 @@
--TEST--
foreach destructuring supports nested and keyed items
--FILE--
<?php
function main(): void
{
$rows = [
['id' => 1, 'pair' => ['a', 'b']],
['id' => 2, 'pair' => ['c', 'd']],
];
foreach ($rows as ['id' => $id, 'pair' => [$left, $right]]) {
var_dump($id, $left, $right);
}
}
?>
--EXPECT--
int(1)
string(1) "a"
string(1) "b"
int(2)
string(1) "c"
string(1) "d"

@ -0,0 +1,31 @@
--TEST--
is_callable with array callables and dynamic method names
--FILE--
<?php
class IsCallableArrayTarget
{
public function run(): void
{
}
public static function stat(): void
{
}
}
function main(): void
{
$object = new IsCallableArrayTarget();
$method = 'run';
$static = 'stat';
var_dump(is_callable([$object, $method]));
var_dump(is_callable([IsCallableArrayTarget::class, $static]));
var_dump(is_callable([$object, 'missing']));
}
?>
--EXPECT--
bool(true)
bool(true)
bool(false)

@ -0,0 +1,31 @@
--TEST--
finally runs before break and continue leave try block
--FILE--
<?php
function main(): void
{
for ($i = 0; $i < 3; $i++) {
try {
echo "try:$i\n";
if ($i === 0) {
continue;
}
if ($i === 1) {
break;
}
} finally {
echo "finally:$i\n";
}
echo "after:$i\n";
}
echo "done\n";
}
?>
--EXPECT--
try:0
finally:0
try:1
finally:1
done

@ -0,0 +1,27 @@
--TEST--
finally runs before break leaves catch block
--FILE--
<?php
function main(): void
{
for ($i = 0; $i < 2; $i++) {
try {
echo "try:$i\n";
throw new RuntimeException('stop');
} catch (RuntimeException $e) {
echo "catch:$i\n";
break;
} finally {
echo "finally:$i\n";
}
}
echo "done\n";
}
?>
--EXPECT--
try:0
catch:0
finally:0
done

@ -0,0 +1,28 @@
--TEST--
finally runs before goto leaves catch block
--FILE--
<?php
function main(): void
{
try {
echo "try\n";
throw new RuntimeException('jump');
} catch (RuntimeException $e) {
echo "catch\n";
goto done;
} finally {
echo "finally\n";
}
echo "unreachable\n";
done:
echo "done\n";
}
?>
--EXPECT--
try
catch
finally
done

@ -0,0 +1,24 @@
--TEST--
finally runs before goto leaves try block
--FILE--
<?php
function main(): void
{
try {
echo "try\n";
goto done;
} finally {
echo "finally\n";
}
echo "unreachable\n";
done:
echo "done\n";
}
?>
--EXPECT--
try
finally
done

@ -0,0 +1,24 @@
--TEST--
finally is not injected before break that only leaves nested loop
--FILE--
<?php
function main(): void
{
try {
echo "try\n";
for ($i = 0; $i < 3; $i++) {
echo "loop:$i\n";
break;
}
echo "after-loop\n";
} finally {
echo "finally\n";
}
}
?>
--EXPECT--
try
loop:0
after-loop
finally

@ -0,0 +1,24 @@
--TEST--
foreach iterator destructuring supports nested and keyed items
--FILE--
<?php
function main(): void
{
$rows = new ArrayObject([
['id' => 10, 'pair' => ['x', 'y']],
['id' => 20, 'pair' => ['m', 'n']],
]);
foreach ($rows as ['id' => $id, 'pair' => [$left, $right]]) {
var_dump($id, $left, $right);
}
}
?>
--EXPECT--
int(10)
string(1) "x"
string(1) "y"
int(20)
string(1) "m"
string(1) "n"

@ -0,0 +1,35 @@
--TEST--
dynamic static method name with side effects
--FILE--
<?php
class DynamicStaticMethodTarget
{
public static function render(string $value): string
{
return 'render:' . $value;
}
}
function choose_static_method(): string
{
echo "method\n";
return 'render';
}
function make_static_arg(): string
{
echo "arg\n";
return 'value';
}
function main(): void
{
$method = choose_static_method();
var_dump(DynamicStaticMethodTarget::$method(make_static_arg()));
}
?>
--EXPECT--
method
arg
string(12) "render:value"

@ -0,0 +1,29 @@
--TEST--
dynamic static property name read and write
--FILE--
<?php
class DynamicStaticPropertyTarget
{
public static int $count = 1;
}
function choose_static_property(): string
{
echo "prop\n";
return 'count';
}
function main(): void
{
$prop = choose_static_property();
var_dump(DynamicStaticPropertyTarget::$$prop);
DynamicStaticPropertyTarget::$$prop = 5;
var_dump(DynamicStaticPropertyTarget::$count);
}
?>
--EXPECT--
prop
int(1)
int(5)

@ -0,0 +1,24 @@
--TEST--
switch default before later matching case
--FILE--
<?php
function main(): void
{
$value = 'target';
switch ($value) {
case 'first':
echo "first\n";
break;
default:
echo "default\n";
break;
case 'target':
echo "target\n";
break;
}
}
?>
--EXPECT--
target
Loading…
Cancel
Save