diff --git a/phpunit/code/constant-overflow-native-add.php b/phpunit/code/constant-overflow-native-add.php new file mode 100644 index 00000000..05257f77 --- /dev/null +++ b/phpunit/code/constant-overflow-native-add.php @@ -0,0 +1,9 @@ + '9223372036854775807 + 1', + 'constant-overflow-native-sub.php' => '-9223372036854775808 - 1', + 'constant-overflow-native-mul.php' => '9223372036854775807 * 2', + 'constant-overflow-native-div.php' => '-9223372036854775808 / -1', + 'constant-overflow-native-mod.php' => '-9223372036854775808 % -1', + 'constant-overflow-native-neg.php' => 'Negating PHP_INT_MIN', + ]; + + foreach ($cases as $file => $expectedMessage) { + try { + $this->compileNativeFile($file); + $this->fail("Expected native constant overflow in {$file} to be rejected"); + } catch (TestError $e) { + $this->assertStringContainsString($expectedMessage, $e->getMessage()); + } + } + + $this->compileNativeFile('constant-overflow-native-ok.php'); + $this->addToAssertionCount(1); + } + /** * @return object{warnings: list} */ @@ -67,4 +91,26 @@ class ConstantArithmeticOverflowTest extends TestCase return $reporter; } + + private function compileNativeFile(string $file): void + { + global $translator; + $compiler = CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $compiler->setDiagnosticReporter(new class implements DiagnosticReporter { + public function fatal(string $message): never + { + throw new TestError($message); + } + + public function warning(Node $node, string $file, string $message): void + { + } + }); + + $testFile = __DIR__ . '/../code/' . $file; + $compiler->addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + } } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 7f86f142..2fabe604 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -241,13 +241,15 @@ class CompilerBase implements PropertyAccessContext protected array $funcMap = []; protected int $propIndex = 0; protected array $propMap = []; + protected const array PHP_RUNTIME_TYPE_MAP = [ + 'integer' => Type::INT, + 'double' => Type::FLOAT, + 'boolean' => Type::BOOL, + ]; protected array $zendTypeMap = [ 'int' => Type::INT, - 'integer' => Type::INT, 'float' => Type::FLOAT, - 'double' => Type::FLOAT, 'bool' => Type::BOOL, - 'boolean' => Type::BOOL, 'false' => Type::BOOL, 'true' => Type::BOOL, 'void' => Type::VOID, @@ -625,7 +627,7 @@ class CompilerBase implements PropertyAccessContext public function getTypeFromZendType(string $type): string { - return $this->zendTypeMap[$type] ?? Type::VAR; + return $this->zendTypeMap[$type] ?? self::PHP_RUNTIME_TYPE_MAP[$type] ?? Type::VAR; } public function getObjectType(string $object): string @@ -2517,7 +2519,16 @@ class CompilerBase implements PropertyAccessContext switch ($exprType) { case 'Expr_UnaryMinus': case 'Expr_UnaryPlus': - return $this->detectTypeOfExpr($expr->expr); + $innerType = $this->detectTypeOfExpr($expr->expr); + if ( + !$this->nativeTypes + && $exprType === 'Expr_UnaryMinus' + && $innerType === Type::INT + && $this->constantIntValue($expr->expr) === PHP_INT_MIN + ) { + return Type::FLOAT; + } + return $innerType; case 'Expr_BooleanNot': case 'Expr_BinaryOp_LogicalAnd': case 'Expr_BinaryOp_BooleanAnd': @@ -2596,6 +2607,22 @@ class CompilerBase implements PropertyAccessContext if ($leftType === Type::FLOAT || $rightType === Type::FLOAT) { return Type::FLOAT; } + if (!$this->nativeTypes && $leftType === Type::INT && $rightType === Type::INT) { + $op = match ($exprType) { + 'Expr_BinaryOp_Plus' => '+', + 'Expr_BinaryOp_Minus' => '-', + 'Expr_BinaryOp_Mul' => '*', + 'Expr_BinaryOp_Div' => '/', + 'Expr_BinaryOp_Mod' => '%', + default => null, + }; + if ($op !== null) { + $evaluation = $this->evaluateConstantIntArithmetic($expr->left, $expr->right, $op); + if ($evaluation !== null && is_float($evaluation['result'])) { + return Type::FLOAT; + } + } + } if ($leftType === Type::INT || $rightType === Type::INT) { return Type::INT; } diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 9f3719e7..a1359113 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -246,20 +246,59 @@ trait BinaryOpTrait } /** - * Fold constant int arithmetic that would overflow int64 in generated C++. + * Fold constant int arithmetic that cannot be emitted as a plain C++ + * signed-integer expression. * - * PHP promotes an overflowing integer operation to float; raw C++ constant - * expressions overflow at compile time (UB) and wrap instead. When both - * operands are compile-time int constants and the PHP result is no longer - * an int, emit the promoted float literal instead of the raw C++ expression. - * With native_types the intentional wrap semantics are kept. + * PHP promotes overflowing arithmetic to float. It also defines + * PHP_INT_MIN % -1 as zero, while the equivalent C++ remainder expression + * has undefined behavior. Native mode rejects every statically detectable + * undefined operation instead of relying on compiler-specific behavior. */ protected function tryFoldConstantIntArithmetic(NodeAbstract $left, NodeAbstract $right, string $op): ?string { + $evaluation = $this->evaluateConstantIntArithmetic($left, $right, $op); + if ($evaluation === null) { + return null; + } + if ($this->nativeTypes) { + if ($evaluation['cppUndefined']) { + $this->fatalError( + $left, + 'Constant integer operation ' . $evaluation['left'] . ' ' . $op . ' ' + . $evaluation['right'] . ' has undefined behavior in C++ native mode' + ); + } return null; } - if (!in_array($op, ['+', '-', '*', '/'], true)) { + + if ($op === '%' && $evaluation['cppUndefined']) { + return $this->genIntegerLiteral($evaluation['result']); + } + + if (is_int($evaluation['result'])) { + return null; + } + + if ($evaluation['cppUndefined']) { + $this->warning( + $left, + 'Constant integer arithmetic overflows int64; folding to PHP float result (' + . $evaluation['left'] . ' ' . $op . ' ' . $evaluation['right'] . ')' + ); + } + return $this->genFloatLiteral($evaluation['result']); + } + + /** + * @return array{left: int, right: int, result: int|float, cppUndefined: bool}|null + */ + protected function evaluateConstantIntArithmetic( + NodeAbstract $left, + NodeAbstract $right, + string $op + ): ?array { + if (!in_array($op, ['+', '-', '*', '/', '%'], true)) { return null; } @@ -268,30 +307,29 @@ trait BinaryOpTrait if ($leftValue === null || $rightValue === null) { return null; } - if ($op === '/' && $rightValue === 0) { - // Division by zero is rejected by guardLiteralDivisionByZero / runtime. + if (($op === '/' || $op === '%') && $rightValue === 0) { + // Division by zero is rejected by guardLiteralDivisionByZero. return null; } - // PHP itself promotes overflowing int arithmetic to float, which is - // exactly the semantics we want for the generated literal. $result = match ($op) { '+' => $leftValue + $rightValue, '-' => $leftValue - $rightValue, '*' => $leftValue * $rightValue, '/' => $leftValue / $rightValue, + '%' => $leftValue % $rightValue, + }; + $cppUndefined = match ($op) { + '+', '-', '*' => is_float($result), + '/', '%' => $leftValue === PHP_INT_MIN && $rightValue === -1, }; - if (is_int($result)) { - // No overflow — keep the plain C++ expression. - return null; - } - $this->warning( - $left, - 'Constant integer arithmetic overflows int64; folding to PHP float result (' - . $leftValue . ' ' . $op . ' ' . $rightValue . ')' - ); - return $this->genFloatLiteral($result); + return [ + 'left' => $leftValue, + 'right' => $rightValue, + 'result' => $result, + 'cppUndefined' => $cppUndefined, + ]; } /** diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index cabfe4f9..a7f1c7d7 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -80,9 +80,15 @@ trait UnaryExpressionTrait if ($type === Type::DECIMAL) { return 'php::Decimal::neg(' . $this->parseExprAsValue($expr->expr) . ')'; } - if (!$this->nativeTypes && $type === Type::INT) { + if ($type === Type::INT) { $value = $this->constantIntValue($expr->expr); if ($value === PHP_INT_MIN) { + if ($this->nativeTypes) { + $this->fatalError( + $expr, + 'Negating PHP_INT_MIN has undefined behavior in C++ native mode' + ); + } // -PHP_INT_MIN overflows int64 and promotes to float in PHP. return $this->genFloatLiteral(-(float) PHP_INT_MIN); } diff --git a/tests/compiler/constant-int-arithmetic-overflow.phpt b/tests/compiler/constant-int-arithmetic-overflow.phpt index ccd2f8b1..d186dfda 100644 --- a/tests/compiler/constant-int-arithmetic-overflow.phpt +++ b/tests/compiler/constant-int-arithmetic-overflow.phpt @@ -4,6 +4,21 @@ Constant integer arithmetic overflow promotes to float like PHP --EXPECTF-- @@ -23,3 +45,14 @@ float(9.223372036854776E+18) int(3) int(9223372036854775807) float(9.223372036854776E+18) +float(9.223372036854776E+18) +bool(false) +bool(true) +float(9.223372036854776E+18) +bool(true) +float(9.223372036854776E+18) +bool(true) +float(0.5) +bool(true) +bool(true) +int(0) diff --git a/tests/compiler/scalar-alias-class-type-names.phpt b/tests/compiler/scalar-alias-class-type-names.phpt new file mode 100644 index 00000000..5a56115d --- /dev/null +++ b/tests/compiler/scalar-alias-class-type-names.phpt @@ -0,0 +1,36 @@ +--TEST-- +integer, boolean and double remain class names in PHP type declarations +--FILE-- + +--EXPECT-- +integer +boolean +double