diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 3360b4de..2f74c585 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -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; diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 8f2a5e96..4daaa4a5 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -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); diff --git a/tests/aot/array/destructure-keyed-write-targets.phpt b/tests/aot/array/destructure-keyed-write-targets.phpt new file mode 100644 index 00000000..351a2f11 --- /dev/null +++ b/tests/aot/array/destructure-keyed-write-targets.phpt @@ -0,0 +1,26 @@ +--TEST-- +keyed destructuring writes to object properties and array dimensions +--FILE-- + $box->name, 'value' => $out['value']] = [ + 'name' => 'alpha', + 'value' => 123, + ]; + + var_dump($box->name, $out['value']); +} +?> +--EXPECT-- +string(5) "alpha" +int(123) diff --git a/tests/aot/array/destructure-keyed.phpt b/tests/aot/array/destructure-keyed.phpt new file mode 100644 index 00000000..cf3ff617 --- /dev/null +++ b/tests/aot/array/destructure-keyed.phpt @@ -0,0 +1,19 @@ +--TEST-- +array destructuring assignment supports keyed and nested items +--FILE-- + $id, 'pair' => [$left, $right]] = [ + 'id' => 42, + 'pair' => ['left', 'right'], + ]; + + var_dump($id, $left, $right); +} +?> +--EXPECT-- +int(42) +string(4) "left" +string(5) "right" diff --git a/tests/aot/array/foreach-list-nested-keyed.phpt b/tests/aot/array/foreach-list-nested-keyed.phpt new file mode 100644 index 00000000..824be02c --- /dev/null +++ b/tests/aot/array/foreach-list-nested-keyed.phpt @@ -0,0 +1,24 @@ +--TEST-- +foreach destructuring supports nested and keyed items +--FILE-- + 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" diff --git a/tests/aot/callable/is-callable-array.phpt b/tests/aot/callable/is-callable-array.phpt new file mode 100644 index 00000000..d35f1afe --- /dev/null +++ b/tests/aot/callable/is-callable-array.phpt @@ -0,0 +1,31 @@ +--TEST-- +is_callable with array callables and dynamic method names +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(false) diff --git a/tests/aot/exception/finally-break-continue.phpt b/tests/aot/exception/finally-break-continue.phpt new file mode 100644 index 00000000..a26b8980 --- /dev/null +++ b/tests/aot/exception/finally-break-continue.phpt @@ -0,0 +1,31 @@ +--TEST-- +finally runs before break and continue leave try block +--FILE-- + +--EXPECT-- +try:0 +finally:0 +try:1 +finally:1 +done diff --git a/tests/aot/exception/finally-catch-break.phpt b/tests/aot/exception/finally-catch-break.phpt new file mode 100644 index 00000000..cb96d235 --- /dev/null +++ b/tests/aot/exception/finally-catch-break.phpt @@ -0,0 +1,27 @@ +--TEST-- +finally runs before break leaves catch block +--FILE-- + +--EXPECT-- +try:0 +catch:0 +finally:0 +done diff --git a/tests/aot/exception/finally-catch-goto.phpt b/tests/aot/exception/finally-catch-goto.phpt new file mode 100644 index 00000000..3c27f1d1 --- /dev/null +++ b/tests/aot/exception/finally-catch-goto.phpt @@ -0,0 +1,28 @@ +--TEST-- +finally runs before goto leaves catch block +--FILE-- + +--EXPECT-- +try +catch +finally +done diff --git a/tests/aot/exception/finally-goto.phpt b/tests/aot/exception/finally-goto.phpt new file mode 100644 index 00000000..9b2a7d76 --- /dev/null +++ b/tests/aot/exception/finally-goto.phpt @@ -0,0 +1,24 @@ +--TEST-- +finally runs before goto leaves try block +--FILE-- + +--EXPECT-- +try +finally +done diff --git a/tests/aot/exception/finally-nested-loop-break.phpt b/tests/aot/exception/finally-nested-loop-break.phpt new file mode 100644 index 00000000..63d101bb --- /dev/null +++ b/tests/aot/exception/finally-nested-loop-break.phpt @@ -0,0 +1,24 @@ +--TEST-- +finally is not injected before break that only leaves nested loop +--FILE-- + +--EXPECT-- +try +loop:0 +after-loop +finally diff --git a/tests/aot/loop/iterator-list-nested-keyed.phpt b/tests/aot/loop/iterator-list-nested-keyed.phpt new file mode 100644 index 00000000..0e485201 --- /dev/null +++ b/tests/aot/loop/iterator-list-nested-keyed.phpt @@ -0,0 +1,24 @@ +--TEST-- +foreach iterator destructuring supports nested and keyed items +--FILE-- + 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" diff --git a/tests/aot/static/static-call-dynamic-method.phpt b/tests/aot/static/static-call-dynamic-method.phpt new file mode 100644 index 00000000..783b00b7 --- /dev/null +++ b/tests/aot/static/static-call-dynamic-method.phpt @@ -0,0 +1,35 @@ +--TEST-- +dynamic static method name with side effects +--FILE-- + +--EXPECT-- +method +arg +string(12) "render:value" diff --git a/tests/aot/static/static-property-dynamic-name.phpt b/tests/aot/static/static-property-dynamic-name.phpt new file mode 100644 index 00000000..2b056cd6 --- /dev/null +++ b/tests/aot/static/static-property-dynamic-name.phpt @@ -0,0 +1,29 @@ +--TEST-- +dynamic static property name read and write +--FILE-- + +--EXPECT-- +prop +int(1) +int(5) diff --git a/tests/aot/switch/default-before-matching-case.phpt b/tests/aot/switch/default-before-matching-case.phpt new file mode 100644 index 00000000..f887be24 --- /dev/null +++ b/tests/aot/switch/default-before-matching-case.phpt @@ -0,0 +1,24 @@ +--TEST-- +switch default before later matching case +--FILE-- + +--EXPECT-- +target