feat(parser): enhance constant expression evaluation with nested operations support

- Add constantNumericValue method to evaluate numeric literals including binary operations
- Implement support for nested constant arithmetic operations in division by zero checks
- Handle constant shift operations with proper overflow detection
- Support native C++17 integer division semantics for constant expressions
- Add comprehensive test coverage for nested constant arithmetic edge cases
- Improve error reporting for undefined behavior in native mode operations
pull/45/head
韩天峰 3 weeks ago
parent 8a23368cf1
commit 2310c574ab
  1. 9
      phpunit/code/constant-overflow-native-div-subtree.php
  2. 9
      phpunit/code/constant-overflow-native-nested-add.php
  3. 9
      phpunit/code/constant-overflow-native-nested-div.php
  4. 9
      phpunit/code/constant-overflow-native-nested-mod.php
  5. 9
      phpunit/code/constant-overflow-native-nested-zero.php
  6. 14
      phpunit/code/constant-overflow-native-nonconstant.php
  7. 1
      phpunit/code/constant-overflow-native-ok.php
  8. 1
      phpunit/code/constant-overflow-warning.php
  9. 9
      phpunit/code/shift-boundary-native-nested-neg-right.php
  10. 9
      phpunit/code/shift-boundary-native-nested-overflow.php
  11. 9
      phpunit/code/shift-boundary-native-wrapped-overflow.php
  12. 8
      phpunit/src/ConstantArithmeticOverflowTest.php
  13. 21
      phpunit/src/ShiftBoundaryTest.php
  14. 131
      src/Parser/BinaryOpTrait.php
  15. 30
      tests/compiler/constant-int-arithmetic-overflow.phpt
  16. 2
      tests/compiler/constant-shift-boundaries.phpt

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

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

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

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

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

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
use native_types;
function addToMaximum(int $value): int
{
return PHP_INT_MAX + $value;
}
function main(): void
{
var_dump(addToMaximum(0));
}

@ -8,5 +8,6 @@ function main(): void
var_dump(PHP_INT_MAX + 0); var_dump(PHP_INT_MAX + 0);
var_dump(PHP_INT_MIN - 0); var_dump(PHP_INT_MIN - 0);
var_dump(4 / 2); var_dump(4 / 2);
var_dump((1 + 1) / 4);
var_dump(5 % 2); var_dump(5 % 2);
} }

@ -7,4 +7,5 @@ function main(): void
$a = PHP_INT_MAX + 1; $a = PHP_INT_MAX + 1;
$b = PHP_INT_MAX * 2; $b = PHP_INT_MAX * 2;
$c = 1 + 2; $c = 1 + 2;
$d = PHP_INT_MAX + (1 - 0);
} }

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

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

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

@ -22,7 +22,7 @@ class ConstantArithmeticOverflowTest extends TestCase
$reporter->warnings, $reporter->warnings,
fn (string $message): bool => str_contains($message, 'Constant integer arithmetic overflows int64') fn (string $message): bool => str_contains($message, 'Constant integer arithmetic overflows int64')
)); ));
$this->assertCount(2, $overflowWarnings); $this->assertCount(3, $overflowWarnings);
$this->assertStringContainsString('9223372036854775807 + 1', $overflowWarnings[0]); $this->assertStringContainsString('9223372036854775807 + 1', $overflowWarnings[0]);
$this->assertStringContainsString('folding to PHP float result', $overflowWarnings[0]); $this->assertStringContainsString('folding to PHP float result', $overflowWarnings[0]);
} }
@ -45,6 +45,11 @@ class ConstantArithmeticOverflowTest extends TestCase
'constant-overflow-native-div.php' => '-9223372036854775808 / -1', 'constant-overflow-native-div.php' => '-9223372036854775808 / -1',
'constant-overflow-native-mod.php' => '-9223372036854775808 % -1', 'constant-overflow-native-mod.php' => '-9223372036854775808 % -1',
'constant-overflow-native-neg.php' => 'Negating PHP_INT_MIN', 'constant-overflow-native-neg.php' => 'Negating PHP_INT_MIN',
'constant-overflow-native-nested-add.php' => '9223372036854775807 + 1',
'constant-overflow-native-nested-div.php' => '-9223372036854775808 / -1',
'constant-overflow-native-nested-mod.php' => '-9223372036854775808 % -1',
'constant-overflow-native-nested-zero.php' => 'Constant division or modulo by zero',
'constant-overflow-native-div-subtree.php' => '9223372036854775807 + 2',
]; ];
foreach ($cases as $file => $expectedMessage) { foreach ($cases as $file => $expectedMessage) {
@ -57,6 +62,7 @@ class ConstantArithmeticOverflowTest extends TestCase
} }
$this->compileNativeFile('constant-overflow-native-ok.php'); $this->compileNativeFile('constant-overflow-native-ok.php');
$this->compileNativeFile('constant-overflow-native-nonconstant.php');
$this->addToAssertionCount(1); $this->addToAssertionCount(1);
} }

@ -57,6 +57,13 @@ class ShiftBoundaryTest extends TestCase
$this->compileNativeWithReporter('shift-boundary-native-overflow.php'); $this->compileNativeWithReporter('shift-boundary-native-overflow.php');
} }
public function testNativeModeRejectsNestedShiftCountAtLeastWordSize(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Bit shift count 64 is >= 64 and is not supported in native mode');
$this->compileNativeWithReporter('shift-boundary-native-nested-overflow.php');
}
public function testNativeModeRejectsNegativeShiftCount(): void public function testNativeModeRejectsNegativeShiftCount(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);
@ -71,6 +78,13 @@ class ShiftBoundaryTest extends TestCase
$this->compileNativeWithReporter('shift-boundary-native-neg-right.php'); $this->compileNativeWithReporter('shift-boundary-native-neg-right.php');
} }
public function testNativeModeRejectsNestedNegativeRightShift(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Right shift of a negative value is implementation-defined in C++');
$this->compileNativeWithReporter('shift-boundary-native-nested-neg-right.php');
}
public function testNativeModeRejectsLeftShiftChangingSignBit(): void public function testNativeModeRejectsLeftShiftChangingSignBit(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);
@ -78,6 +92,13 @@ class ShiftBoundaryTest extends TestCase
$this->compileNativeWithReporter('shift-boundary-native-sign-bit.php'); $this->compileNativeWithReporter('shift-boundary-native-sign-bit.php');
} }
public function testNativeModeRejectsLeftShiftThatWrapsPastSignBit(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Left shift that changes the sign bit is undefined behavior in C++');
$this->compileNativeWithReporter('shift-boundary-native-wrapped-overflow.php');
}
public function testNativeModeRejectsNegativeLeftShift(): void public function testNativeModeRejectsNegativeLeftShift(): void
{ {
$this->expectException(TestError::class); $this->expectException(TestError::class);

@ -128,6 +128,16 @@ trait BinaryOpTrait
$this->guardLiteralDivisionByZero($right, $op); $this->guardLiteralDivisionByZero($right, $op);
$constantDivisionByZero = $this->handleNestedConstantDivisionByZero(
$right,
$op,
$leftExpr,
$rightExpr
);
if ($constantDivisionByZero !== null) {
return $constantDivisionByZero;
}
if ($op === '%' and !($leftType === Type::INT and $rightType === Type::INT)) { if ($op === '%' and !($leftType === Type::INT and $rightType === Type::INT)) {
return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')'; return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')';
} }
@ -240,9 +250,13 @@ trait BinaryOpTrait
if ($value < 0 || $shift <= 0) { if ($value < 0 || $shift <= 0) {
return false; return false;
} }
// PHP shift is performed on unsigned values; a negative result means if ($shift >= PHP_INT_SIZE * 8) {
// the sign bit got set. return true;
return ($value << $shift) < 0; }
// Avoid performing the overflowing shift while checking it. Testing
// the wrapped PHP result misses cases such as 2 << 63, which becomes
// zero in PHP but is undefined/implementation-defined in C++17.
return $value > (PHP_INT_MAX >> $shift);
} }
/** /**
@ -337,20 +351,30 @@ trait BinaryOpTrait
* expression is not a statically known int constant. * expression is not a statically known int constant.
*/ */
protected function constantIntValue(NodeAbstract $expr): ?int protected function constantIntValue(NodeAbstract $expr): ?int
{
$value = $this->constantNumericValue($expr, $this->nativeTypes);
return is_int($value) ? $value : null;
}
/**
* Evaluate a numeric literal tree without touching dynamic expressions.
* Native mode uses C++17 integer-division semantics so an enclosing
* operation can still be checked for undefined behavior.
*/
protected function constantNumericValue(NodeAbstract $expr, bool $nativeSemantics): int|float|null
{ {
if ($expr instanceof Node\Scalar\Int_) { if ($expr instanceof Node\Scalar\Int_) {
return $expr->value; return $expr->value;
} }
if ($expr instanceof Node\Scalar\Float_) {
return $expr->value;
}
if ($expr instanceof Node\Expr\UnaryPlus) { if ($expr instanceof Node\Expr\UnaryPlus) {
return $this->constantIntValue($expr->expr); return $this->constantNumericValue($expr->expr, $nativeSemantics);
} }
if ($expr instanceof Node\Expr\UnaryMinus) { if ($expr instanceof Node\Expr\UnaryMinus) {
$value = $this->constantIntValue($expr->expr); $value = $this->constantNumericValue($expr->expr, $nativeSemantics);
if ($value === null || $value === PHP_INT_MIN) { return $value === null ? null : -$value;
// -PHP_INT_MIN promotes to float in PHP; not an int constant.
return null;
}
return -$value;
} }
if ($expr instanceof Node\Expr\ConstFetch) { if ($expr instanceof Node\Expr\ConstFetch) {
$name = strtolower($expr->name->toString()); $name = strtolower($expr->name->toString());
@ -360,8 +384,95 @@ trait BinaryOpTrait
default => null, default => null,
}; };
} }
if (!$expr instanceof Node\Expr\BinaryOp) {
return null;
}
$left = $this->constantNumericValue($expr->left, $nativeSemantics);
$right = $this->constantNumericValue($expr->right, $nativeSemantics);
if ($left === null || $right === null) {
return null;
}
return match (true) {
$expr instanceof Node\Expr\BinaryOp\Plus => $left + $right,
$expr instanceof Node\Expr\BinaryOp\Minus => $left - $right,
$expr instanceof Node\Expr\BinaryOp\Mul => $left * $right,
$expr instanceof Node\Expr\BinaryOp\Div => $this->constantDivisionValue(
$left,
$right,
$nativeSemantics
),
$expr instanceof Node\Expr\BinaryOp\Mod => is_int($left) && is_int($right) && $right !== 0
? $left % $right
: null,
$expr instanceof Node\Expr\BinaryOp\BitwiseAnd => is_int($left) && is_int($right)
? $left & $right
: null,
$expr instanceof Node\Expr\BinaryOp\BitwiseOr => is_int($left) && is_int($right)
? $left | $right
: null,
$expr instanceof Node\Expr\BinaryOp\BitwiseXor => is_int($left) && is_int($right)
? $left ^ $right
: null,
$expr instanceof Node\Expr\BinaryOp\ShiftLeft => is_int($left) && is_int($right)
? $this->constantShiftValue($left, $right, true)
: null,
$expr instanceof Node\Expr\BinaryOp\ShiftRight => is_int($left) && is_int($right)
? $this->constantShiftValue($left, $right, false)
: null,
default => null,
};
}
protected function constantDivisionValue(int|float $left, int|float $right, bool $nativeSemantics): int|float|null
{
if ($right == 0) {
return null;
}
if ($nativeSemantics && is_int($left) && is_int($right)) {
if ($left === PHP_INT_MIN && $right === -1) {
return null;
}
return intdiv($left, $right);
}
return $left / $right;
}
protected function constantShiftValue(int $value, int $shift, bool $left): ?int
{
if ($shift < 0) {
return null; return null;
} }
if ($shift >= PHP_INT_SIZE * 8) {
return $left ? 0 : ($value < 0 ? -1 : 0);
}
return $left ? $value << $shift : $value >> $shift;
}
protected function handleNestedConstantDivisionByZero(
NodeAbstract $right,
string $op,
string $leftExpr,
string $rightExpr
): ?string {
if (($op !== '/' && $op !== '%') || $this->isZeroLiteral($right)) {
return null;
}
$rightValue = $this->constantNumericValue($right, $this->nativeTypes);
if ($rightValue === null || $rightValue != 0) {
return null;
}
if ($this->nativeTypes) {
$this->fatalError($right, 'Constant division or modulo by zero has undefined behavior in C++ native mode');
}
// Preserve PHP's catchable DivisionByZeroError for a nested constant
// zero. Literal zero keeps the compiler's established diagnostic.
return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))';
}
protected function genFloatLiteral(float $value): string protected function genFloatLiteral(float $value): string
{ {

@ -35,6 +35,25 @@ function main(): void
var_dump(fractionalDivisionInferred(), is_float(fractionalDivisionInferred())); var_dump(fractionalDivisionInferred(), is_float(fractionalDivisionInferred()));
var_dump((PHP_INT_MAX + 1) === (float) PHP_INT_MAX); var_dump((PHP_INT_MAX + 1) === (float) PHP_INT_MAX);
var_dump(PHP_INT_MIN % -1); var_dump(PHP_INT_MIN % -1);
var_dump((1 + 1) / 4);
var_dump(PHP_INT_MAX + (1 - 0));
var_dump(PHP_INT_MIN - (1 + 0));
var_dump(PHP_INT_MAX * (1 + 1));
var_dump(PHP_INT_MIN / (0 - 1));
var_dump(PHP_INT_MIN % (0 - 1));
var_dump(-(PHP_INT_MIN + 0));
var_dump(PHP_INT_MAX - (1 + 0));
var_dump((4 / 2) + PHP_INT_MAX);
try {
var_dump(8 / (1 - 1));
} catch (DivisionByZeroError $error) {
echo "nested division by zero\n";
}
try {
var_dump(8 % (1 - 1));
} catch (DivisionByZeroError $error) {
echo "nested modulo by zero\n";
}
} }
?> ?>
--EXPECTF-- --EXPECTF--
@ -56,3 +75,14 @@ float(0.5)
bool(true) bool(true)
bool(true) bool(true)
int(0) int(0)
float(0.5)
float(9.223372036854776E+18)
float(-9.223372036854776E+18)
float(1.8446744073709552E+19)
float(9.223372036854776E+18)
int(0)
float(9.223372036854776E+18)
int(9223372036854775806)
float(9.223372036854776E+18)
nested division by zero
nested modulo by zero

@ -14,6 +14,7 @@ function main(): void
var_dump(-1 >> 63); var_dump(-1 >> 63);
var_dump(1 << 63); var_dump(1 << 63);
var_dump(5 >> 1); var_dump(5 >> 1);
var_dump(1 >> (32 + 32));
try { try {
var_dump(1 >> -1); var_dump(1 >> -1);
} catch (ArithmeticError $e) { } catch (ArithmeticError $e) {
@ -30,4 +31,5 @@ int(0)
int(-1) int(-1)
int(-9223372036854775808) int(-9223372036854775808)
int(2) int(2)
int(0)
caught: Bit shift by negative number caught: Bit shift by negative number

Loading…
Cancel
Save