diff --git a/phpunit/code/constant-overflow-native-div-subtree.php b/phpunit/code/constant-overflow-native-div-subtree.php new file mode 100644 index 00000000..7078d974 --- /dev/null +++ b/phpunit/code/constant-overflow-native-div-subtree.php @@ -0,0 +1,9 @@ +> (1 + 1)); +} diff --git a/phpunit/code/shift-boundary-native-nested-overflow.php b/phpunit/code/shift-boundary-native-nested-overflow.php new file mode 100644 index 00000000..82f63c6d --- /dev/null +++ b/phpunit/code/shift-boundary-native-nested-overflow.php @@ -0,0 +1,9 @@ +warnings, 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('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-mod.php' => '-9223372036854775808 % -1', '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) { @@ -57,6 +62,7 @@ class ConstantArithmeticOverflowTest extends TestCase } $this->compileNativeFile('constant-overflow-native-ok.php'); + $this->compileNativeFile('constant-overflow-native-nonconstant.php'); $this->addToAssertionCount(1); } diff --git a/phpunit/src/ShiftBoundaryTest.php b/phpunit/src/ShiftBoundaryTest.php index 3f7ea29e..a906f13e 100644 --- a/phpunit/src/ShiftBoundaryTest.php +++ b/phpunit/src/ShiftBoundaryTest.php @@ -57,6 +57,13 @@ class ShiftBoundaryTest extends TestCase $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 { $this->expectException(TestError::class); @@ -71,6 +78,13 @@ class ShiftBoundaryTest extends TestCase $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 { $this->expectException(TestError::class); @@ -78,6 +92,13 @@ class ShiftBoundaryTest extends TestCase $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 { $this->expectException(TestError::class); diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index a1359113..014db900 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -128,6 +128,16 @@ trait BinaryOpTrait $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)) { return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')'; } @@ -240,9 +250,13 @@ trait BinaryOpTrait if ($value < 0 || $shift <= 0) { return false; } - // PHP shift is performed on unsigned values; a negative result means - // the sign bit got set. - return ($value << $shift) < 0; + if ($shift >= PHP_INT_SIZE * 8) { + return true; + } + // 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. */ 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_) { return $expr->value; } + if ($expr instanceof Node\Scalar\Float_) { + return $expr->value; + } if ($expr instanceof Node\Expr\UnaryPlus) { - return $this->constantIntValue($expr->expr); + return $this->constantNumericValue($expr->expr, $nativeSemantics); } 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; + $value = $this->constantNumericValue($expr->expr, $nativeSemantics); + return $value === null ? null : -$value; } if ($expr instanceof Node\Expr\ConstFetch) { $name = strtolower($expr->name->toString()); @@ -360,7 +384,94 @@ trait BinaryOpTrait default => null, }; } - return 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; + } + 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 diff --git a/tests/compiler/constant-int-arithmetic-overflow.phpt b/tests/compiler/constant-int-arithmetic-overflow.phpt index d186dfda..79b6d8c1 100644 --- a/tests/compiler/constant-int-arithmetic-overflow.phpt +++ b/tests/compiler/constant-int-arithmetic-overflow.phpt @@ -35,6 +35,25 @@ function main(): void var_dump(fractionalDivisionInferred(), is_float(fractionalDivisionInferred())); var_dump((PHP_INT_MAX + 1) === (float) PHP_INT_MAX); 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-- @@ -56,3 +75,14 @@ float(0.5) bool(true) bool(true) 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 diff --git a/tests/compiler/constant-shift-boundaries.phpt b/tests/compiler/constant-shift-boundaries.phpt index 3ea27138..6b389326 100644 --- a/tests/compiler/constant-shift-boundaries.phpt +++ b/tests/compiler/constant-shift-boundaries.phpt @@ -14,6 +14,7 @@ function main(): void var_dump(-1 >> 63); var_dump(1 << 63); var_dump(5 >> 1); + var_dump(1 >> (32 + 32)); try { var_dump(1 >> -1); } catch (ArithmeticError $e) { @@ -30,4 +31,5 @@ int(0) int(-1) int(-9223372036854775808) int(2) +int(0) caught: Bit shift by negative number