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 @@ +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/phpunit/src/OperatorTest.php b/phpunit/src/OperatorTest.php index 7bb2d19d..04f2c2bf 100644 --- a/phpunit/src/OperatorTest.php +++ b/phpunit/src/OperatorTest.php @@ -2,6 +2,26 @@ class OperatorTest extends \BaseTest { + public function testBooleanLiteralStrictComparisonUsesNativeBoolOperands(): void + { + global $translator; + $compiler = \TypePhp\CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $testFile = __DIR__ . '/../code/bool-literal-identical.php'; + $compiler->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/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/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/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/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) diff --git a/version.txt b/version.txt index e50f8eba..ba851d84 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1096 \ No newline at end of file +1096