feat(parser): enhance constant integer arithmetic handling with native mode checks

- Refactor tryFoldConstantIntArithmetic to use separate evaluation method
- Add native mode rejection for undefined C++ operations like PHP_INT_MIN % -1
- Support modulo operator in constant arithmetic evaluation
- Add proper type mapping for integer/double/boolean in runtime type system
- Handle unary minus overflow detection when native types are disabled
- Implement division by zero handling for both division and modulo operations
- Add comprehensive tests for native mode constant overflow behavior
- Create test cases for scalar alias class type name handling
- Add test files for native mode arithmetic overflow validation
pull/45/head
韩天峰 3 weeks ago
parent c6c04d7e96
commit a7b16dc1be
  1. 9
      phpunit/code/constant-overflow-native-add.php
  2. 9
      phpunit/code/constant-overflow-native-div.php
  3. 9
      phpunit/code/constant-overflow-native-mod.php
  4. 9
      phpunit/code/constant-overflow-native-mul.php
  5. 9
      phpunit/code/constant-overflow-native-neg.php
  6. 12
      phpunit/code/constant-overflow-native-ok.php
  7. 9
      phpunit/code/constant-overflow-native-sub.php
  8. 46
      phpunit/src/ConstantArithmeticOverflowTest.php
  9. 37
      src/CompilerBase.php
  10. 80
      src/Parser/BinaryOpTrait.php
  11. 8
      src/Parser/UnaryExpressionTrait.php
  12. 33
      tests/compiler/constant-int-arithmetic-overflow.phpt
  13. 36
      tests/compiler/scalar-alias-class-type-names.phpt

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
var_dump(PHP_INT_MAX + 1);
}

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
var_dump(PHP_INT_MIN / -1);
}

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
var_dump(PHP_INT_MIN % -1);
}

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
var_dump(PHP_INT_MAX * 2);
}

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
var_dump(-PHP_INT_MIN);
}

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
var_dump(PHP_INT_MAX + 0);
var_dump(PHP_INT_MIN - 0);
var_dump(4 / 2);
var_dump(5 % 2);
}

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
use native_types;
function main(): void
{
var_dump(PHP_INT_MIN - 1);
}

@ -36,6 +36,30 @@ class ConstantArithmeticOverflowTest extends TestCase
}
}
public function testNativeModeRejectsConstantUndefinedBehavior(): void
{
$cases = [
'constant-overflow-native-add.php' => '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<string>}
*/
@ -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);
}
}

@ -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;
}

@ -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,
];
}
/**

@ -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);
}

@ -4,6 +4,21 @@ Constant integer arithmetic overflow promotes to float like PHP
<?php
declare(strict_types=1);
function overflowInferred()
{
return PHP_INT_MAX + 1;
}
function unaryOverflowInferred()
{
return -PHP_INT_MIN;
}
function fractionalDivisionInferred()
{
return 1 / 2;
}
function main(): void
{
var_dump(PHP_INT_MAX + 1);
@ -13,6 +28,13 @@ function main(): void
var_dump(1 + 2);
var_dump(PHP_INT_MAX + 0);
var_dump(-PHP_INT_MIN);
$overflow = PHP_INT_MAX + 1;
var_dump($overflow, is_int($overflow), is_float($overflow));
var_dump(overflowInferred(), is_float(overflowInferred()));
var_dump(unaryOverflowInferred(), is_float(unaryOverflowInferred()));
var_dump(fractionalDivisionInferred(), is_float(fractionalDivisionInferred()));
var_dump((PHP_INT_MAX + 1) === (float) PHP_INT_MAX);
var_dump(PHP_INT_MIN % -1);
}
?>
--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)

@ -0,0 +1,36 @@
--TEST--
integer, boolean and double remain class names in PHP type declarations
--FILE--
<?php
declare(strict_types=1);
class integer {}
class boolean {}
class double {}
function acceptInteger(integer $value): string
{
return get_class($value);
}
function acceptBoolean(boolean $value): string
{
return get_class($value);
}
function acceptDouble(double $value): string
{
return get_class($value);
}
function main(): void
{
echo acceptInteger(new integer()), "\n";
echo acceptBoolean(new boolean()), "\n";
echo acceptDouble(new double()), "\n";
}
?>
--EXPECT--
integer
boolean
double
Loading…
Cancel
Save