修复位移操作中未定义行为

pull/45/head
韩天峰 3 weeks ago
parent f2c9309857
commit 78135d75ef
  1. 10
      phpunit/code/constant-overflow-warning.php
  2. 9
      phpunit/code/shift-boundary-native-neg-left.php
  3. 9
      phpunit/code/shift-boundary-native-neg-right.php
  4. 9
      phpunit/code/shift-boundary-native-negative.php
  5. 11
      phpunit/code/shift-boundary-native-ok.php
  6. 9
      phpunit/code/shift-boundary-native-overflow.php
  7. 9
      phpunit/code/shift-boundary-native-sign-bit.php
  8. 13
      phpunit/code/shift-boundary-warning.php
  9. 70
      phpunit/src/ConstantArithmeticOverflowTest.php
  10. 149
      phpunit/src/ShiftBoundaryTest.php
  11. 110
      src/Parser/BinaryOpTrait.php
  12. 33
      tests/compiler/constant-shift-boundaries.phpt

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
function main(): void
{
$a = PHP_INT_MAX + 1;
$b = PHP_INT_MAX * 2;
$c = 1 + 2;
}

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

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

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

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

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

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

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
function main(): void
{
$a = 1 << 64;
$b = 1 >> 64;
$c = -1 >> 64;
$d = 1 >> -1;
$e = 1 << 2;
$f = 3 >> 1;
}

@ -0,0 +1,70 @@
<?php
namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use PhpParser\Node;
use TypePhp\CompilerTest;
use TypePhp\Diagnostics\DiagnosticReporter;
use TypePhp\Exception\TestError;
/**
* Constant integer arithmetic overflow emits compile-time warnings and folds
* to the PHP float result in non-native mode.
*/
class ConstantArithmeticOverflowTest extends TestCase
{
public function testOverflowingConstantArithmeticEmitsWarning(): void
{
$reporter = $this->compileWithReporter();
$overflowWarnings = array_values(array_filter(
$reporter->warnings,
fn (string $message): bool => str_contains($message, 'Constant integer arithmetic overflows int64')
));
$this->assertCount(2, $overflowWarnings);
$this->assertStringContainsString('9223372036854775807 + 1', $overflowWarnings[0]);
$this->assertStringContainsString('folding to PHP float result', $overflowWarnings[0]);
}
public function testNonOverflowingConstantArithmeticDoesNotWarn(): void
{
$reporter = $this->compileWithReporter();
foreach ($reporter->warnings as $message) {
$this->assertStringNotContainsString('1 + 2', $message);
}
}
/**
* @return object{warnings: list<string>}
*/
private function compileWithReporter(): object
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$reporter = new class implements DiagnosticReporter {
/** @var list<string> */
public array $warnings = [];
public function fatal(string $message): never
{
throw new TestError($message);
}
public function warning(Node $node, string $file, string $message): void
{
$this->warnings[] = $message;
}
};
$compiler->setDiagnosticReporter($reporter);
$testFile = __DIR__ . '/../code/constant-overflow-warning.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
return $reporter;
}
}

@ -0,0 +1,149 @@
<?php
namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase;
use PhpParser\Node;
use TypePhp\CompilerTest;
use TypePhp\Diagnostics\DiagnosticReporter;
use TypePhp\Exception\TestError;
/**
* Constant bit shift boundaries emit compile-time warnings and keep PHP
* semantics in non-native mode.
*/
class ShiftBoundaryTest extends TestCase
{
public function testShiftCountAtLeastWordSizeEmitsWarning(): void
{
$reporter = $this->compileWithReporter();
$overflowWarnings = array_values(array_filter(
$reporter->warnings,
fn (string $message): bool => str_contains($message, 'Bit shift count 64 is >= 64')
));
$this->assertCount(3, $overflowWarnings);
foreach ($overflowWarnings as $message) {
$this->assertStringContainsString('folding with PHP semantics', $message);
}
}
public function testNegativeShiftCountEmitsWarning(): void
{
$reporter = $this->compileWithReporter();
$negativeWarnings = array_values(array_filter(
$reporter->warnings,
fn (string $message): bool => str_contains($message, 'Bit shift by a negative number')
));
$this->assertCount(1, $negativeWarnings);
$this->assertStringContainsString('ArithmeticError', $negativeWarnings[0]);
}
public function testInRangeShiftDoesNotWarn(): void
{
$reporter = $this->compileWithReporter();
foreach ($reporter->warnings as $message) {
$this->assertStringNotContainsString('Bit shift count 2 is >=', $message);
$this->assertStringNotContainsString('Bit shift count 1 is >=', $message);
}
}
public function testNativeModeRejectsShiftCountAtLeastWordSize(): 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-overflow.php');
}
public function testNativeModeRejectsNegativeShiftCount(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Bit shift by a negative number is not supported in native mode');
$this->compileNativeWithReporter('shift-boundary-native-negative.php');
}
public function testNativeModeRejectsNegativeRightShift(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Right shift of a negative value is implementation-defined in C++');
$this->compileNativeWithReporter('shift-boundary-native-neg-right.php');
}
public function testNativeModeRejectsLeftShiftChangingSignBit(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Left shift that changes the sign bit is undefined behavior in C++');
$this->compileNativeWithReporter('shift-boundary-native-sign-bit.php');
}
public function testNativeModeRejectsNegativeLeftShift(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage('Left shift of a negative value is undefined behavior in C++');
$this->compileNativeWithReporter('shift-boundary-native-neg-left.php');
}
public function testNativeModeAllowsInRangeShift(): void
{
$compiler = $this->compileNativeWithReporter('shift-boundary-native-ok.php');
$this->assertNotNull($compiler);
}
/**
* @return object{warnings: list<string>}
*/
private function compileWithReporter(): object
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$reporter = new class implements DiagnosticReporter {
/** @var list<string> */
public array $warnings = [];
public function fatal(string $message): never
{
throw new TestError($message);
}
public function warning(Node $node, string $file, string $message): void
{
$this->warnings[] = $message;
}
};
$compiler->setDiagnosticReporter($reporter);
$testFile = __DIR__ . '/../code/shift-boundary-warning.php';
$compiler->addFiles([$testFile]);
$compiler->prepareFile($testFile);
$compiler->convertFile($testFile);
return $reporter;
}
private function compileNativeWithReporter(string $file): CompilerTest
{
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);
return $compiler;
}
}

@ -132,6 +132,13 @@ trait BinaryOpTrait
return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')';
}
if ($op === '<<' || $op === '>>') {
$foldedShift = $this->tryFoldConstantShift($left, $right, $op, $leftExpr, $rightExpr);
if ($foldedShift !== null) {
return $foldedShift;
}
}
$folded = $this->tryFoldConstantIntArithmetic($left, $right, $op);
if ($folded !== null) {
return $folded;
@ -140,6 +147,104 @@ trait BinaryOpTrait
return '((' . $leftExpr . ') ' . $op . ' (' . $rightExpr . '))';
}
/**
* Fold constant integer shifts to PHP semantics in non-native mode.
*
* PHP shifts by >= word size to 0 (left) or -1/0 (right, arithmetic), and
* throws a catchable ArithmeticError for negative shift counts. Native C++
* shifts are undefined for those counts, so the constant case is folded
* (>= word size) or routed through php::Var (negative, so the Zend shift
* function raises the catchable error at runtime).
*/
protected function tryFoldConstantShift(
NodeAbstract $left,
NodeAbstract $right,
string $op,
string $leftExpr,
string $rightExpr
): ?string {
$leftValue = $this->constantIntValue($left);
$shiftValue = $this->constantIntValue($right);
if ($leftValue === null || $shiftValue === null) {
return null;
}
$wordSize = PHP_INT_SIZE * 8;
if ($this->nativeTypes) {
if ($shiftValue >= $wordSize) {
$this->fatalError(
$right,
'Bit shift count ' . $shiftValue . ' is >= ' . $wordSize
. ' and is not supported in native mode'
);
}
if ($shiftValue < 0) {
$this->fatalError(
$right,
'Bit shift by a negative number is not supported in native mode'
);
}
if ($op === '>>' && $leftValue < 0) {
$this->fatalError(
$left,
'Right shift of a negative value is implementation-defined in C++'
. ' and is not supported in native mode'
);
}
if ($op === '<<' && $leftValue < 0) {
$this->fatalError(
$left,
'Left shift of a negative value is undefined behavior in C++'
. ' and is not supported in native mode'
);
}
if ($op === '<<' && $this->leftShiftTouchesSignBit($leftValue, $shiftValue)) {
$this->fatalError(
$left,
'Left shift that changes the sign bit is undefined behavior in C++'
. ' and is not supported in native mode'
);
}
return null;
}
if ($shiftValue >= $wordSize) {
$result = $op === '<<' ? '0LL' : ($leftValue < 0 ? '-1LL' : '0LL');
$this->warning(
$right,
'Bit shift count ' . $shiftValue . ' is >= ' . $wordSize
. '; folding with PHP semantics (left shift to 0, right shift to -1 for negative operands, 0 otherwise)'
);
return $result;
}
if ($shiftValue < 0) {
$this->warning(
$right,
'Bit shift by a negative number throws ArithmeticError at runtime'
);
// Route through php::Var so the Zend shift function raises the catchable error.
return '((php::Var(' . $leftExpr . ')) ' . $op . ' (php::Var(' . $rightExpr . ')))';
}
return null;
}
/**
* Whether a constant left shift of a non-negative value would set the sign
* bit (overflow the signed range), which is undefined behavior in C++.
*/
protected function leftShiftTouchesSignBit(int $value, int $shift): bool
{
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;
}
/**
* Fold constant int arithmetic that would overflow int64 in generated C++.
*
@ -181,6 +286,11 @@ trait BinaryOpTrait
return null;
}
$this->warning(
$left,
'Constant integer arithmetic overflows int64; folding to PHP float result ('
. $leftValue . ' ' . $op . ' ' . $rightValue . ')'
);
return $this->genFloatLiteral($result);
}

@ -0,0 +1,33 @@
--TEST--
Constant bit shift boundaries follow PHP semantics
--FILE--
<?php
declare(strict_types=1);
function main(): void
{
var_dump(1 << 64);
var_dump(1 >> 64);
var_dump(-1 >> 64);
var_dump(PHP_INT_MIN >> 64);
var_dump(1 >> 63);
var_dump(-1 >> 63);
var_dump(1 << 63);
var_dump(5 >> 1);
try {
var_dump(1 >> -1);
} catch (ArithmeticError $e) {
echo "caught: " . $e->getMessage() . "\n";
}
}
?>
--EXPECTF--
int(0)
int(0)
int(-1)
int(-1)
int(0)
int(-1)
int(-9223372036854775808)
int(2)
caught: Bit shift by negative number
Loading…
Cancel
Save