diff --git a/phpunit/src/TraitsTest.php b/phpunit/src/TraitsTest.php index ab4a7eaa..c4081985 100644 --- a/phpunit/src/TraitsTest.php +++ b/phpunit/src/TraitsTest.php @@ -95,29 +95,6 @@ class TraitsTest extends TestCase $this->assertFalse($this->invoke('checkArgType', 'void', 'php::Str')); } - // ======================================================================== - // FuncCallOptimizer::isValidDefineName - // ======================================================================== - - public function testIsValidDefineNameValid(): void - { - $this->assertTrue($this->invoke('isValidDefineName', 'MY_CONSTANT')); - $this->assertTrue($this->invoke('isValidDefineName', 'APP_NAME')); - $this->assertTrue($this->invoke('isValidDefineName', '_PRIVATE')); - $this->assertTrue($this->invoke('isValidDefineName', 'camelCase')); - $this->assertTrue($this->invoke('isValidDefineName', 'Test123')); - $this->assertTrue($this->invoke('isValidDefineName', '_')); - } - - public function testIsValidDefineNameInvalid(): void - { - $this->assertFalse($this->invoke('isValidDefineName', '123abc')); // starts with digit - $this->assertFalse($this->invoke('isValidDefineName', 'has space')); // contains space - $this->assertFalse($this->invoke('isValidDefineName', 'has-dash')); // contains dash - $this->assertFalse($this->invoke('isValidDefineName', '')); // empty - $this->assertFalse($this->invoke('isValidDefineName', '0abc')); // starts with zero - } - // ======================================================================== // StdContainerParser::isStdContainerType // ======================================================================== diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 4c84660c..46d4671c 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1516,6 +1516,7 @@ class CompilerBase implements PropertyAccessContext if ($cond instanceof Expr\Assign) { $condExpr = '(' . $condExpr . ')'; } + $condExpr = $this->convertConditionExpr($cond, $condExpr); $code .= $openPrefix . '(' . $condExpr . ') {' . PHP_EOL; return $code; } @@ -2513,7 +2514,23 @@ class CompilerBase implements PropertyAccessContext $exprType = $expr->getType(); switch ($exprType) { case 'Expr_UnaryMinus': + case 'Expr_UnaryPlus': return $this->detectTypeOfExpr($expr->expr); + case 'Expr_BooleanNot': + case 'Expr_BinaryOp_LogicalAnd': + case 'Expr_BinaryOp_BooleanAnd': + case 'Expr_BinaryOp_LogicalOr': + case 'Expr_BinaryOp_BooleanOr': + case 'Expr_BinaryOp_LogicalXor': + case 'Expr_BinaryOp_Equal': + case 'Expr_BinaryOp_NotEqual': + case 'Expr_BinaryOp_Identical': + case 'Expr_BinaryOp_NotIdentical': + case 'Expr_BinaryOp_Smaller': + case 'Expr_BinaryOp_SmallerOrEqual': + case 'Expr_BinaryOp_Greater': + case 'Expr_BinaryOp_GreaterOrEqual': + return Type::BOOL; case 'Expr_BitwiseNot': $inner = $this->detectTypeOfExpr($expr->expr); return $inner === Type::BIGINT ? Type::BIGINT : Type::INT; @@ -2542,6 +2559,12 @@ class CompilerBase implements PropertyAccessContext case 'Expr_BinaryOp_Concat': case 'Expr_AssignOp_Concat': return Type::STR; + case 'Expr_Ternary': + $ifType = $expr->if === null + ? $this->detectTypeOfExpr($expr->cond) + : $this->detectTypeOfExpr($expr->if); + $elseType = $this->detectTypeOfExpr($expr->else); + return $ifType === $elseType ? $ifType : Type::VAR; case 'Expr_BinaryOp_Plus': case 'Expr_BinaryOp_Minus': case 'Expr_BinaryOp_Mul': @@ -2553,7 +2576,6 @@ class CompilerBase implements PropertyAccessContext case 'Expr_BinaryOp_BitwiseAnd': case 'Expr_BinaryOp_BitwiseOr': case 'Expr_BinaryOp_BitwiseXor': - case 'Expr_BinaryOp_BooleanAnd': $leftType = $this->detectTypeOfExpr($expr->left); $rightType = $this->detectTypeOfExpr($expr->right); if ($leftType === Type::BIGFLOAT || $rightType === Type::BIGFLOAT) { @@ -3449,6 +3471,10 @@ class CompilerBase implements PropertyAccessContext protected function parseEmpty(Expr\Empty_ $expr): string { + $type = $this->detectTypeOfExpr($expr->expr); + if (in_array($type, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) { + return '!(' . $this->convertBoolExpr($this->parseExprAsValue($expr->expr), $type) . ')'; + } return $this->parseChainedExpr($expr->expr, self::OP_EMPTY); } diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 5effdfef..6454a905 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -474,9 +474,10 @@ trait BinaryOpTrait $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $rightAfterStmtCount); $this->checkVarMustExist($right, $rightExpr); - $leftBool = $this->convertBoolExpr((string) $leftExpr); + $leftBool = $this->convertBoolExpr((string) $leftExpr, $this->detectTypeOfExpr($left)); + $rightBool = $this->convertBoolExpr((string) $rightExpr, $this->detectTypeOfExpr($right)); if (!$rightBeforeStmts && !$rightAfterStmts) { - return '(' . $leftBool . ' ' . $op . ' ' . $this->convertBoolExpr((string) $rightExpr) . ')'; + return '(' . $leftBool . ' ' . $op . ' ' . $rightBool . ')'; } $shortCircuitValue = $op === '&&' ? 'false' : 'true'; @@ -490,8 +491,9 @@ trait BinaryOpTrait $code .= $this->getIndent() . $rightTmpVar . ' = ' . $rightExpr . ';'; $code .= $this->formatCapturedStmtLines($rightAfterStmts); $rightExpr = $rightTmpVar; + $rightBool = $this->convertBoolExpr($rightExpr, $this->detectTypeOfExpr($right)); } - $code .= $this->getIndent() . 'return ' . $this->convertBoolExpr((string) $rightExpr) . ';'; + $code .= $this->getIndent() . 'return ' . $rightBool . ';'; $code .= $this->getIndent() . '}'; $code .= $this->getIndent() . 'return ' . $shortCircuitValue . ';'; $code .= $this->getIndent() . '}()'; @@ -501,7 +503,13 @@ trait BinaryOpTrait protected function parseBinaryOpLogicalXor(Expr\BinaryOp\LogicalXor $expr): string { - return $this->convertBoolExpr($this->parseBinaryOp($expr->left, $expr->right, '^')); + $this->assertExprCanBeUsedAsCondition($expr->left, 'logical operand'); + $this->assertExprCanBeUsedAsCondition($expr->right, 'logical operand'); + $left = $this->parseOrderedBinaryOperand($expr->left); + $right = $this->parseOrderedBinaryOperand($expr->right); + $leftBool = $this->convertBoolExpr($left, $this->detectTypeOfExpr($expr->left)); + $rightBool = $this->convertBoolExpr($right, $this->detectTypeOfExpr($expr->right)); + return '(' . $leftBool . ' != ' . $rightBool . ')'; } protected function parseBinaryOpSmallerOrEqual(Expr\BinaryOp\SmallerOrEqual $expr): string diff --git a/src/Parser/LoopControlTrait.php b/src/Parser/LoopControlTrait.php index 8819e6d4..1e90883c 100644 --- a/src/Parser/LoopControlTrait.php +++ b/src/Parser/LoopControlTrait.php @@ -42,8 +42,8 @@ trait LoopControlTrait [$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); $condExpr = $this->stringifyParsedExpr($condExpr); $hasCondStmts = $hasCondStmts || $beforeStmts || $afterStmts; - $list_cond[] = [$condExpr, $beforeStmts, $afterStmts]; - $list_cond_expr[] = $condExpr; + $list_cond[] = [$expr, $condExpr, $beforeStmts, $afterStmts]; + $list_cond_expr[] = $this->convertConditionExpr($expr, $condExpr); } $code .= $this->parseBeforeStmtLines() . PHP_EOL; @@ -55,7 +55,7 @@ trait LoopControlTrait } else { $condResult = $this->genTmpVarName(); $condCode .= $this->getIndent() . 'bool ' . $condResult . ' = true;' . PHP_EOL; - foreach ($list_cond as [$condExpr, $beforeStmts, $afterStmts]) { + foreach ($list_cond as [$condNode, $condExpr, $beforeStmts, $afterStmts]) { $condCode .= $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { $tmpVar = $this->addTmpVar(Type::VAR); @@ -63,7 +63,7 @@ trait LoopControlTrait $condCode .= $this->formatCapturedStmtLines($afterStmts); $condExpr = $tmpVar; } - $condCode .= $this->getIndent() . $condResult . ' = ' . $this->convertBoolExpr($condExpr) . ';' . PHP_EOL; + $condCode .= $this->getIndent() . $condResult . ' = ' . $this->convertConditionExpr($condNode, $condExpr) . ';' . PHP_EOL; } $condCode .= $this->getIndent() . 'return ' . $condResult . ';'; } @@ -122,8 +122,10 @@ trait LoopControlTrait $code .= $this->formatCapturedStmtLines($afterStmts); $cond = $tmpVar; } + $cond = $this->convertConditionExpr($v->cond, $cond); $code .= $this->getIndent() . 'if (!(' . $cond . ')) { break; }' . PHP_EOL; } else { + $cond = $this->convertConditionExpr($v->cond, $cond); $code .= 'while (' . $cond . ') {' . PHP_EOL; } $code .= $this->parseBlockStmts($stmts); @@ -148,9 +150,11 @@ trait LoopControlTrait $condCode .= $this->formatCapturedStmtLines($afterStmts); $cond = $tmpVar; } - $condCode .= $this->getIndent() . 'return ' . $this->convertBoolExpr($cond) . ';'; + $condCode .= $this->getIndent() . 'return ' . $this->convertConditionExpr($v->cond, $cond) . ';'; $condCode .= $this->getIndent() . '}()'; $cond = $condCode; + } else { + $cond = $this->convertConditionExpr($v->cond, $cond); } $code = $this->parseBeforeStmtLines() . PHP_EOL; $code .= 'do {' . PHP_EOL; diff --git a/src/Parser/SelectionExpressionTrait.php b/src/Parser/SelectionExpressionTrait.php index 9cd801e4..e9d38ca2 100644 --- a/src/Parser/SelectionExpressionTrait.php +++ b/src/Parser/SelectionExpressionTrait.php @@ -54,6 +54,7 @@ trait SelectionExpressionTrait $code .= $this->formatCapturedStmtLines($condAfterStmts); $cond = $condTmpVar; } + $cond = $this->convertConditionExpr($expr->cond, $cond); $code .= $this->getIndent() . 'if (' . $cond . ') {'; $code .= $this->formatTernaryReturn($if, $ifBeforeStmts, $ifAfterStmts); $code .= $this->getIndent() . '} else {'; @@ -62,6 +63,7 @@ trait SelectionExpressionTrait $code .= $this->getIndent() . '}()'; return $code; } + $cond = $this->convertConditionExpr($expr->cond, $cond); return '(' . $cond . ') ? (' . $if . ') : (' . $else . ')'; } @@ -167,6 +169,11 @@ trait SelectionExpressionTrait if ($chainOpResult) { $leftExpr = $chainOpResult; } + $leftType = $this->detectTypeOfExpr($left); + if ($op === self::OP_NOT_EMPTY + && in_array($leftType, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) { + $condExpr = '((' . $condExpr . '), ' . $this->convertBoolExpr($leftExpr, $leftType) . ')'; + } $rightBeforeStmtCount = count($this->context->beforeStmtLines); $rightAfterStmtCount = count($this->context->afterStmtLines); diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 1845591c..1180e05d 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -162,6 +162,9 @@ trait TypeConversionTrait protected function convertBoolExpr(string $expr, string $fromType = ''): string { + if ($fromType === Type::BOOL) { + return $expr; + } $bigConversion = match ($fromType) { Type::BIGINT => 'php::BigInt::toBool', Type::BIGFLOAT => 'php::BigFloat::toBool', @@ -178,6 +181,15 @@ trait TypeConversionTrait return $expr; } + protected function convertConditionExpr(NodeAbstract $node, string $expr): string + { + $type = $this->detectTypeOfExpr($node); + if (in_array($type, [Type::BIGINT, Type::BIGFLOAT, Type::DECIMAL], true)) { + return $this->convertBoolExpr($expr, $type); + } + return $expr; + } + protected function convertExprType(string $expr, $leftType, $rightType): string { if ($leftType === Type::FLOAT or $rightType === Type::FLOAT) { diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index c0ba7d93..0d10a610 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -28,7 +28,10 @@ trait UnaryExpressionTrait protected function parseBooleanNot(Expr\BooleanNot $expr): string { $this->assertExprCanBeUsedAsCondition($expr->expr, 'boolean operand'); - return '!(' . $this->parseExprAsValue($expr->expr) . ')'; + return '!(' . $this->convertBoolExpr( + $this->parseExprAsValue($expr->expr), + $this->detectTypeOfExpr($expr->expr) + ) . ')'; } protected function parseCastInt(Expr\Cast\Int_ $node): string diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 07125648..97c20dfb 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -149,10 +149,7 @@ class Preprocessor extends CompilerBase fn (Node $node, string $message) => $this->warning($node, $message), $this->file, )); - $traverser->addVisitor(new ConstantExpressionValidationVisitor( - $this->phpVersion, - fn (Node $node, string $message) => $this->fatalError($node, $message), - )); + $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $stmts = $traverser->traverse($ast); diff --git a/src/gen_stub.php b/src/gen_stub.php index e2cb90e8..779d39c8 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -3007,6 +3007,8 @@ class ConstInfo extends VariableLike return "\tZEND_ASSERT($cExpr == NULL);\n"; } + $cValue = null; + if ($value->type->isBool()) { $cValue = $constValue ? "true" : "false"; return "\tZEND_ASSERT($cExpr == $cValue);\n"; diff --git a/tests/compiler/bignumber/logical-and-unary-operators.phpt b/tests/compiler/bignumber/logical-and-unary-operators.phpt new file mode 100644 index 00000000..ed2a4b72 --- /dev/null +++ b/tests/compiler/bignumber/logical-and-unary-operators.phpt @@ -0,0 +1,70 @@ +--TEST-- +Big numeric unary plus and boolean contexts use numeric truth values +--FILE-- +toString(), "\n"; + var_dump(!$bi0, !$bi1); + var_dump($bi0 && $bi1, $bi0 || $bi1, ($bi1 xor $bi1), ($bi0 xor $bi1)); + + $dec0 = std::decimal("0.0"); + $dec1 = std::decimal("1.0"); + echo (+$dec1)->toString(), "\n"; + var_dump(!$dec0, !$dec1); + var_dump($dec0 && $dec1, $dec0 || $dec1, ($dec1 xor $dec1), ($dec0 xor $dec1)); + + $bf0 = std::bigFloat("0"); + $bf1 = std::bigFloat("1"); + echo (+$bf1)->toString(), "\n"; + var_dump(!$bf0, !$bf1); + var_dump($bf0 && $bf1, $bf0 || $bf1, ($bf1 xor $bf1), ($bf0 xor $bf1)); + + echo $bi0 ? "bad-if\n" : "if-ok\n"; + echo $dec0 ? "bad-ternary\n" : "ternary-ok\n"; + echo ($bf0 ?: std::bigFloat("9"))->toString(), "\n"; + var_dump(empty($bi0), empty($bi1)); + + while ($bi0) { + echo "bad-while\n"; + } + for (; $dec0;) { + echo "bad-for\n"; + } + do { + echo "do-once\n"; + } while ($bf0); +} +?> +--EXPECT-- +1 +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +1.0 +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +1 +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +if-ok +ternary-ok +9 +bool(true) +bool(false) +do-once diff --git a/tests/compiler/bignumber/operator-error-boundaries.phpt b/tests/compiler/bignumber/operator-error-boundaries.phpt new file mode 100644 index 00000000..59e72133 --- /dev/null +++ b/tests/compiler/bignumber/operator-error-boundaries.phpt @@ -0,0 +1,52 @@ +--TEST-- +Big numeric operator error boundaries use PHP-compatible exception types +--FILE-- + std::decimal("1"); + } catch (ArithmeticError $e) { + echo "decimal NaN comparison\n"; + } + + try { + $unused = std::bigFloat("NAN") <=> std::bigFloat("1"); + } catch (ArithmeticError $e) { + echo "bigfloat NaN comparison\n"; + } +} +?> +--EXPECT-- +bigint division by zero +bigint modulo by zero +bigint negative exponent +decimal modulo by zero +decimal NaN comparison +bigfloat NaN comparison diff --git a/tests/compiler/bignumber/reverse-and-compound-operators.phpt b/tests/compiler/bignumber/reverse-and-compound-operators.phpt new file mode 100644 index 00000000..68438136 --- /dev/null +++ b/tests/compiler/bignumber/reverse-and-compound-operators.phpt @@ -0,0 +1,42 @@ +--TEST-- +Big numeric reverse non-commutative and BigFloat compound operators +--FILE-- +toString(), "\n"; + echo (10 / $bi)->toString(), "\n"; + echo (10 % $bi)->toString(), "\n"; + echo (std::bigInt(-7) / 3)->toString(), "\n"; + echo (std::bigInt(-7) % 3)->toString(), "\n"; + + $dec = std::decimal("4.0"); + echo (10 - $dec)->toString(), "\n"; + echo (10 / $dec)->toString(), "\n"; + echo (10 % $dec)->toString(), "\n"; + + $bf = std::bigFloat("4.0"); + echo (10 - $bf)->toString(), "\n"; + echo (10 / $bf)->toString(), "\n"; + $bf += 2; + $bf -= 1; + $bf *= 3; + $bf /= 5; + echo $bf->toString(), "\n"; +} +?> +--EXPECT-- +6 +2 +2 +-2 +-1 +6.0 +2.5 +2.0 +6 +2.5 +3