diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 46d4671c..7f86f142 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -243,9 +243,11 @@ class CompilerBase implements PropertyAccessContext protected array $propMap = []; 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, diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 6454a905..e40cb3ef 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -132,9 +132,99 @@ trait BinaryOpTrait return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')'; } + $folded = $this->tryFoldConstantIntArithmetic($left, $right, $op); + if ($folded !== null) { + return $folded; + } + return '((' . $leftExpr . ') ' . $op . ' (' . $rightExpr . '))'; } + /** + * Fold constant int arithmetic that would overflow int64 in generated C++. + * + * 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. + */ + protected function tryFoldConstantIntArithmetic(NodeAbstract $left, NodeAbstract $right, string $op): ?string + { + if ($this->nativeTypes) { + return null; + } + if (!in_array($op, ['+', '-', '*', '/'], true)) { + return null; + } + + $leftValue = $this->constantIntValue($left); + $rightValue = $this->constantIntValue($right); + if ($leftValue === null || $rightValue === null) { + return null; + } + if ($op === '/' && $rightValue === 0) { + // Division by zero is rejected by guardLiteralDivisionByZero / runtime. + 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, + }; + if (is_int($result)) { + // No overflow — keep the plain C++ expression. + return null; + } + + return $this->genFloatLiteral($result); + } + + /** + * Resolve a compile-time integer constant value, or null when the + * expression is not a statically known int constant. + */ + protected function constantIntValue(NodeAbstract $expr): ?int + { + if ($expr instanceof Node\Scalar\Int_) { + return $expr->value; + } + if ($expr instanceof Node\Expr\UnaryPlus) { + return $this->constantIntValue($expr->expr); + } + if ($expr instanceof Node\Expr\UnaryMinus) { + $value = $this->constantIntValue($expr->expr); + if ($value === null || $value === PHP_INT_MIN) { + // -PHP_INT_MIN promotes to float in PHP; not an int constant. + return null; + } + return -$value; + } + if ($expr instanceof Node\Expr\ConstFetch) { + $name = strtolower($expr->name->toString()); + return match ($name) { + 'php_int_max' => PHP_INT_MAX, + 'php_int_min' => PHP_INT_MIN, + default => null, + }; + } + return null; + } + + protected function genFloatLiteral(float $value): string + { + $text = sprintf('%.17g', $value); + // Make sure the literal is parsed as a C++ double. + if (!str_contains($text, '.') && !str_contains(strtolower($text), 'e')) { + $text .= '.0'; + } + return $text; + } + protected function shouldMaterializeOrderedOperand(NodeAbstract $expr): bool { if ($expr instanceof Expr\BinaryOp) { diff --git a/src/Parser/UnaryExpressionTrait.php b/src/Parser/UnaryExpressionTrait.php index 0d10a610..cabfe4f9 100644 --- a/src/Parser/UnaryExpressionTrait.php +++ b/src/Parser/UnaryExpressionTrait.php @@ -80,6 +80,13 @@ trait UnaryExpressionTrait if ($type === Type::DECIMAL) { return 'php::Decimal::neg(' . $this->parseExprAsValue($expr->expr) . ')'; } + if (!$this->nativeTypes && $type === Type::INT) { + $value = $this->constantIntValue($expr->expr); + if ($value === PHP_INT_MIN) { + // -PHP_INT_MIN overflows int64 and promotes to float in PHP. + return $this->genFloatLiteral(-(float) PHP_INT_MIN); + } + } $code = $this->parseExprAsValue($expr->expr); return '-' . $code; diff --git a/tests/compiler/constant-int-arithmetic-overflow.phpt b/tests/compiler/constant-int-arithmetic-overflow.phpt new file mode 100644 index 00000000..ccd2f8b1 --- /dev/null +++ b/tests/compiler/constant-int-arithmetic-overflow.phpt @@ -0,0 +1,25 @@ +--TEST-- +Constant integer arithmetic overflow promotes to float like PHP +--FILE-- + +--EXPECTF-- +float(9.223372036854776E+18) +float(-9.223372036854776E+18) +float(1.8446744073709552E+19) +float(9.223372036854776E+18) +int(3) +int(9223372036854775807) +float(9.223372036854776E+18)