From 9a7de8ee31a1611de778761fc482fef0edc04d0a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 31 Jul 2026 16:31:45 +0800 Subject: [PATCH 1/2] feat(parser): add continue statement support in switch cases within loops - Add isContinueExpr method to AstNodeType for continue statement detection - Introduce inContinuableLoop context flag to track for/foreach/while/do-while loops - Update CompilerBase to maintain continuable loop state during compilation - Modify LoopControlTrait to validate continue statements only within continuable loops - Enhance SwitchTrait to allow continue as valid case ending expression - Add comprehensive tests for continue in switch nested in various loop types - Create test cases for invalid continue usage outside loops - Update version number from 1094 to 1095 --- .../control-flow/loop-switch-continue.php | 53 +++++++++++++++++++ .../standalone-dynamic-switch-continue.php | 11 ++++ .../standalone-switch-continue.php | 13 +++++ phpunit/src/LoopControlTest.php | 25 +++++++++ src/CompilerBase.php | 9 ++++ src/Context/FunctionContext.php | 5 ++ src/Parser/AstNodeType.php | 5 ++ src/Parser/LoopControlTrait.php | 2 +- src/Parser/SwitchTrait.php | 3 +- version.txt | 2 +- 10 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 phpunit/code/control-flow/loop-switch-continue.php create mode 100644 phpunit/code/control-flow/standalone-dynamic-switch-continue.php create mode 100644 phpunit/code/control-flow/standalone-switch-continue.php create mode 100644 phpunit/src/LoopControlTest.php diff --git a/phpunit/code/control-flow/loop-switch-continue.php b/phpunit/code/control-flow/loop-switch-continue.php new file mode 100644 index 00000000..44f7faf7 --- /dev/null +++ b/phpunit/code/control-flow/loop-switch-continue.php @@ -0,0 +1,53 @@ +exec( + 'Cannot continue outside loop', + 'control-flow/standalone-switch-continue.php', + ); + } + + public function testContinueInStandaloneDynamicSwitchIsRejected(): void + { + $this->exec( + 'Cannot continue outside loop', + 'control-flow/standalone-dynamic-switch-continue.php', + ); + } + + public function testContinueInSwitchNestedInLoopsIsAllowed(): void + { + $this->compile('control-flow/loop-switch-continue.php'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 5a2f709a..37922f79 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1516,6 +1516,7 @@ class CompilerBase implements PropertyAccessContext $this->context->enterScope(); $lines = []; $inLoopTop = $this->context->inLoop; + $inContinuableLoopTop = $this->context->inContinuableLoop; $last = array_key_last($stmts); foreach ($stmts as $i => $v) { $class = $v->getType(); @@ -1545,13 +1546,17 @@ class CompilerBase implements PropertyAccessContext break; case 'Stmt_For': $this->context->inLoop = true; + $this->context->inContinuableLoop = true; $result = $this->parseFor($v); $this->context->inLoop = $inLoopTop; + $this->context->inContinuableLoop = $inContinuableLoopTop; break; case 'Stmt_Foreach': $this->context->inLoop = true; + $this->context->inContinuableLoop = true; $result = $this->parseForeach($v); $this->context->inLoop = $inLoopTop; + $this->context->inContinuableLoop = $inContinuableLoopTop; break; case 'Stmt_Switch': $this->context->inLoop = true; @@ -1560,13 +1565,17 @@ class CompilerBase implements PropertyAccessContext break; case 'Stmt_While': $this->context->inLoop = true; + $this->context->inContinuableLoop = true; $result = $this->parseWhile($v); $this->context->inLoop = $inLoopTop; + $this->context->inContinuableLoop = $inContinuableLoopTop; break; case 'Stmt_Do': $this->context->inLoop = true; + $this->context->inContinuableLoop = true; $result = $this->parseDo($v); $this->context->inLoop = $inLoopTop; + $this->context->inContinuableLoop = $inContinuableLoopTop; break; case 'Stmt_If': $result = $this->parseIf($v); diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index f966c933..3b979f67 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -55,7 +55,10 @@ class FunctionContext public array $ceWrappers = []; public int $tmpVarIndex = 0; public array $arguments = []; + /** True while parsing a breakable loop or switch. */ public bool $inLoop = false; + /** True while parsing a for/foreach/while/do-while body. */ + public bool $inContinuableLoop = false; public bool $inClosure = false; public ?array $closureReturnTypeCheck = null; public string $closureReturnTypeStr = ''; @@ -97,6 +100,7 @@ class FunctionContext $this->scopeLayouts = []; $this->scopeLevel = 0; $this->inLoop = false; + $this->inContinuableLoop = false; $this->inClosure = false; $this->closureReturnTypeCheck = null; $this->closureReturnTypeStr = ''; @@ -127,5 +131,6 @@ class FunctionContext $this->scopeLayouts = []; $this->scopeLevel = 0; $this->inLoop = false; + $this->inContinuableLoop = false; } } diff --git a/src/Parser/AstNodeType.php b/src/Parser/AstNodeType.php index f01d5047..1d01a80f 100644 --- a/src/Parser/AstNodeType.php +++ b/src/Parser/AstNodeType.php @@ -152,6 +152,11 @@ trait AstNodeType return $expr instanceof Node\Stmt\Break_; } + protected function isContinueExpr(NodeAbstract $expr): bool + { + return $expr instanceof Node\Stmt\Continue_; + } + protected function isThrowExpr(NodeAbstract $expr): bool { if ($expr instanceof Node\Stmt\Expression) { diff --git a/src/Parser/LoopControlTrait.php b/src/Parser/LoopControlTrait.php index de32676e..8819e6d4 100644 --- a/src/Parser/LoopControlTrait.php +++ b/src/Parser/LoopControlTrait.php @@ -183,7 +183,7 @@ trait LoopControlTrait protected function parseContinue(Node\Stmt\Continue_ $v): string { - if (!$this->context->inLoop) { + if (!$this->context->inContinuableLoop) { $this->fatalError($v, 'Cannot continue outside loop'); } $num = $v->num; diff --git a/src/Parser/SwitchTrait.php b/src/Parser/SwitchTrait.php index 33d2b6a1..293cf324 100644 --- a/src/Parser/SwitchTrait.php +++ b/src/Parser/SwitchTrait.php @@ -90,9 +90,10 @@ trait SwitchTrait if (!$this->isReturnExpr($lastExpr) and !$this->isExitExpr($lastExpr) and !$this->isBreakExpr($lastExpr) + and !$this->isContinueExpr($lastExpr) and !$this->isThrowExpr($lastExpr) ) { - $this->fatalError($case, 'switch case must end with return/break/exit/throw, ' . $lastExpr->getType() . ' given'); + $this->fatalError($case, 'switch case must end with return/break/continue/exit/throw, ' . $lastExpr->getType() . ' given'); } $target = count($caseGroups); if ($hasDefault) { diff --git a/version.txt b/version.txt index 87e831c3..fc15416f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1094 \ No newline at end of file +1095 From 8f2e63b9f71c07f56458df9a249d19718bf31351 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 31 Jul 2026 16:38:32 +0800 Subject: [PATCH 2/2] fix(parser): handle native boolean literals in strict comparisons - Added nativeBoolLiteral method to convert PHP boolean literals to C++ bool - Updated parseCompareExpr to use native boolean literals for bool type - Ensured strict comparisons between booleans use true/false instead of php::true_/php::false_ - Added test case for boolean literal identical comparisons - Created compiler test to verify native boolean operand usage - Added test file with boolean literal comparison examples --- phpunit/code/bool-literal-identical.php | 16 +++++++++ phpunit/src/OperatorTest.php | 20 +++++++++++ src/Parser/BinaryOpTrait.php | 18 ++++++++++ .../operator/bool-literal-identical.phpt | 34 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 phpunit/code/bool-literal-identical.php create mode 100644 tests/compiler/operator/bool-literal-identical.phpt diff --git a/phpunit/code/bool-literal-identical.php b/phpunit/code/bool-literal-identical.php new file mode 100644 index 00000000..7bd3d9ea --- /dev/null +++ b/phpunit/code/bool-literal-identical.php @@ -0,0 +1,16 @@ +addFiles([$testFile]); + $compiler->prepareFile($testFile); + $cppFile = $compiler->convertFile($testFile); + $cpp = file_get_contents($cppFile); + + $this->assertStringContainsString('true == pjax', $cpp); + $this->assertStringContainsString('pjax == true', $cpp); + $this->assertStringContainsString('false == pjax', $cpp); + $this->assertStringContainsString('pjax == false', $cpp); + $this->assertStringNotContainsString('php::true_ == pjax', $cpp); + $this->assertStringNotContainsString('php::false_ == pjax', $cpp); + $this->assertStringContainsString('php::same(php::true_, value)', $cpp); + } + public function testLiteralIntDivideByZeroDoesNotCompile(): void { $this->exec('Cannot divide or modulo by zero', 'divide-by-zero-int.php'); diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 83332cb4..feae8dcb 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -414,11 +414,29 @@ trait BinaryOpTrait return null; } if ($leftType === $rightType) { + if ($leftType === Type::BOOL) { + $cppLeft = $this->nativeBoolLiteral($astLeft) ?? $cppLeft; + $cppRight = $this->nativeBoolLiteral($astRight) ?? $cppRight; + } return $cppLeft . ' == ' . $cppRight; } return 'false'; } + /** + * Strict comparisons between native booleans must use C++ bool literals. + * parseCompareExpr() normally emits php::true_/php::false_ Variants because + * dynamic comparisons need zvals, but those wrappers are incorrect once + * optimizeIdenticalOp() selects a direct primitive comparison. + */ + private function nativeBoolLiteral(NodeAbstract $expr): ?string + { + if (!$this->isScalarBool($expr)) { + return null; + } + return strcasecmp($expr->name->toString(), 'true') === 0 ? 'true' : 'false'; + } + protected function parseBinaryOpLogicalAnd(Expr\BinaryOp\LogicalAnd|Expr\BinaryOp\BooleanAnd $expr): string { return $this->parseShortCircuitLogicalOp($expr->left, $expr->right, '&&'); diff --git a/tests/compiler/operator/bool-literal-identical.phpt b/tests/compiler/operator/bool-literal-identical.phpt new file mode 100644 index 00000000..d73ec833 --- /dev/null +++ b/tests/compiler/operator/bool-literal-identical.phpt @@ -0,0 +1,34 @@ +--TEST-- +Boolean literals on either side of strict comparisons +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true)