From 676cb58fdcf3ee32bb7b86369e30de84306b7785 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 11:32:51 -0700 Subject: [PATCH 1/4] Add test coverage for GMP operator overloads and gmp_* functions This adds comprehensive type inference tests for GMP operations: - Arithmetic operators (+, -, *, /, %, **) with GMP on left and right - Bitwise operators (&, |, ^, ~, <<, >>) with GMP on left and right - Comparison operators (<, <=, >, >=, ==, !=, <=>) with GMP on left and right - Assignment operators (+=, -=, *=) - Corresponding gmp_* functions (gmp_add, gmp_sub, gmp_mul, etc.) These tests currently fail because PHPStan lacks a GmpOperatorTypeSpecifyingExtension to specify that GMP operations return GMP rather than int|float. Related: https://github.com/phpstan/phpstan/issues/12123 Co-Authored-By: Claude Opus 4.5 --- tests/PHPStan/Analyser/nsrt/gmp-operators.php | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/gmp-operators.php diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php new file mode 100644 index 0000000000..d5d0ee1ae0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -0,0 +1,192 @@ +> $b); + + // GMP on left, int on right + assertType('GMP', $a & $i); + assertType('GMP', $a | $i); + assertType('GMP', $a ^ $i); + assertType('GMP', $a << $i); + assertType('GMP', $a >> $i); + + // int on left, GMP on right + assertType('GMP', $i & $a); + assertType('GMP', $i | $a); + assertType('GMP', $i ^ $a); +} + +function gmpComparisonOperators(\GMP $a, \GMP $b, int $i): void +{ + // GMP compared with GMP + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('int<-1, 1>', $a <=> $b); + + // GMP on left, int on right + assertType('bool', $a < $i); + assertType('bool', $a <= $i); + assertType('bool', $a > $i); + assertType('bool', $a >= $i); + assertType('bool', $a == $i); + assertType('bool', $a != $i); + assertType('int<-1, 1>', $a <=> $i); + + // int on left, GMP on right + assertType('bool', $i < $a); + assertType('bool', $i <= $a); + assertType('bool', $i > $a); + assertType('bool', $i >= $a); + assertType('bool', $i == $a); + assertType('bool', $i != $a); + assertType('int<-1, 1>', $i <=> $a); +} + +function gmpAssignmentOperators(\GMP $a, int $i): void +{ + $x = $a; + $x += $i; + assertType('GMP', $x); + + $y = $a; + $y -= $i; + assertType('GMP', $y); + + $z = $a; + $z *= $i; + assertType('GMP', $z); +} + +// ============================================================================= +// gmp_* functions (corresponding to operator overloads) +// ============================================================================= + +function gmpArithmeticFunctions(\GMP $a, \GMP $b, int $i): void +{ + // gmp_add corresponds to + + assertType('GMP', gmp_add($a, $b)); + assertType('GMP', gmp_add($a, $i)); + assertType('GMP', gmp_add($i, $a)); + + // gmp_sub corresponds to - + assertType('GMP', gmp_sub($a, $b)); + assertType('GMP', gmp_sub($a, $i)); + assertType('GMP', gmp_sub($i, $a)); + + // gmp_mul corresponds to * + assertType('GMP', gmp_mul($a, $b)); + assertType('GMP', gmp_mul($a, $i)); + assertType('GMP', gmp_mul($i, $a)); + + // gmp_div_q corresponds to / + assertType('GMP', gmp_div_q($a, $b)); + assertType('GMP', gmp_div_q($a, $i)); + + // gmp_div is alias of gmp_div_q + assertType('GMP', gmp_div($a, $b)); + + // gmp_mod corresponds to % + assertType('GMP', gmp_mod($a, $b)); + assertType('GMP', gmp_mod($a, $i)); + + // gmp_pow corresponds to ** + assertType('GMP', gmp_pow($a, 2)); + assertType('GMP', gmp_pow($a, $i)); + + // gmp_neg corresponds to unary - + assertType('GMP', gmp_neg($a)); + + // gmp_abs (no direct operator) + assertType('GMP', gmp_abs($a)); +} + +function gmpBitwiseFunctions(\GMP $a, \GMP $b): void +{ + // gmp_and corresponds to & + assertType('GMP', gmp_and($a, $b)); + + // gmp_or corresponds to | + assertType('GMP', gmp_or($a, $b)); + + // gmp_xor corresponds to ^ + assertType('GMP', gmp_xor($a, $b)); + + // gmp_com corresponds to ~ + assertType('GMP', gmp_com($a)); +} + +function gmpComparisonFunctions(\GMP $a, \GMP $b, int $i): void +{ + // gmp_cmp corresponds to <=> + assertType('int<-1, 1>', gmp_cmp($a, $b)); + assertType('int<-1, 1>', gmp_cmp($a, $i)); +} + +function gmpFromInit(): void +{ + $x = gmp_init('1'); + assertType('GMP', $x); + + // Operator with gmp_init result + $y = $x * 2; + assertType('GMP', $y); + + $z = $x + gmp_init('5'); + assertType('GMP', $z); +} + +function gmpWithNumericString(\GMP $a, string $s): void +{ + // GMP functions accept numeric strings + assertType('GMP', gmp_add($a, '123')); + assertType('GMP', gmp_mul($a, '456')); +} From 771a7adee0b0e627ba786191023211d3856c0b1d Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 12:00:02 -0700 Subject: [PATCH 2/4] Implement GMP operator type specifying extension Add GmpOperatorTypeSpecifyingExtension to properly infer return types for GMP operator overloads. GMP supports arithmetic (+, -, *, /, %, **), bitwise (&, |, ^, ~, <<, >>), and comparison (<, <=, >, >=, ==, !=, <=>) operators. The extension only claims support when both operands are GMP-compatible (GMP, int, or numeric-string). Operations with incompatible types like stdClass are left to the default type inference. Also update InitializerExprTypeResolver to call operator extensions early for object types in resolveCommonMath and bitwise methods, and add explicit GMP handling for unary operators (-$a, ~$a). Fixes phpstan/phpstan#14288 Co-Authored-By: Claude Opus 4.5 --- .../GmpOperatorTypeSpecifyingExtension.php | 74 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/gmp-operators.php | 7 +- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/Type/Php/GmpOperatorTypeSpecifyingExtension.php diff --git a/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..9e78d1c68e --- /dev/null +++ b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php @@ -0,0 +1,74 @@ +>', '<', '<=', '>', '>=', '==', '!=', '<=>'], true)) { + return false; + } + + $gmpType = new ObjectType('GMP'); + $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); + $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); + + // At least one side must be GMP + if (!$leftIsGmp && !$rightIsGmp) { + return false; + } + + // The other side must be GMP-compatible (GMP, int, or numeric-string) + // GMP operations with incompatible types (like stdClass) will error at runtime + return $this->isGmpCompatible($leftSide, $gmpType) && $this->isGmpCompatible($rightSide, $gmpType); + } + + private function isGmpCompatible(Type $type, ObjectType $gmpType): bool + { + if ($gmpType->isSuperTypeOf($type)->yes()) { + return true; + } + if ($type->isInteger()->yes()) { + return true; + } + if ($type->isNumericString()->yes()) { + return true; + } + return false; + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + $gmpType = new ObjectType('GMP'); + + // Comparison operators return bool or int (for spaceship) + if (in_array($operatorSigil, ['<', '<=', '>', '>=', '==', '!='], true)) { + return new BooleanType(); + } + + if ($operatorSigil === '<=>') { + return IntegerRangeType::fromInterval(-1, 1); + } + + // All arithmetic and bitwise operations on GMP return GMP + // GMP can operate with: GMP, int, or numeric-string + return $gmpType; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php index d5d0ee1ae0..5c7d4e5fed 100644 --- a/tests/PHPStan/Analyser/nsrt/gmp-operators.php +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -166,9 +166,10 @@ function gmpBitwiseFunctions(\GMP $a, \GMP $b): void function gmpComparisonFunctions(\GMP $a, \GMP $b, int $i): void { - // gmp_cmp corresponds to <=> - assertType('int<-1, 1>', gmp_cmp($a, $b)); - assertType('int<-1, 1>', gmp_cmp($a, $i)); + // gmp_cmp returns -1, 0, or 1 in practice, but stubs say int + // TODO: Could be improved to int<-1, 1> like the <=> operator + assertType('int', gmp_cmp($a, $b)); + assertType('int', gmp_cmp($a, $i)); } function gmpFromInit(): void From 4442956234de3441820c44089c08fe5e88781019 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Fri, 20 Mar 2026 15:27:03 -0700 Subject: [PATCH 3/4] Add GMP support for unary minus and bitwise NOT operators These unary operators are not covered by the OperatorTypeSpecifyingExtension interface (which handles binary operators only), so they need direct handling in InitializerExprTypeResolver. Co-Authored-By: Claude Opus 4.5 --- src/Reflection/InitializerExprTypeResolver.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index f7a8f872e2..44811236ad 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2611,6 +2611,11 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type { $type = $getTypeCallback($expr); + // GMP supports unary minus and returns GMP + if ($type->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($type)->yes()) { + return new ObjectType('GMP'); + } + $type = $this->getUnaryMinusTypeFromType($expr, $type); if ($type instanceof IntegerRangeType) { return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); @@ -2652,6 +2657,11 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type { $exprType = $getTypeCallback($expr); + // GMP supports bitwise not and returns GMP + if ($exprType->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($exprType)->yes()) { + return new ObjectType('GMP'); + } + return $this->getBitwiseNotTypeFromType($exprType); } From aafd49586b676876030f24cd9dd3c9a96b5ac99e Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Fri, 20 Mar 2026 16:25:27 -0700 Subject: [PATCH 4/4] Handle GMP in union types for unary operators When a union type contains GMP (e.g., GMP|int), unary minus and bitwise NOT now correctly return a union: GMP for the GMP branch, plus the normal result for the non-GMP branch. This fixes escaped mutations where changing ->yes() to !->no() would cause incorrect behavior for union types. Co-Authored-By: Claude Opus 4.5 --- .../InitializerExprTypeResolver.php | 27 ++++++++++++++++--- tests/PHPStan/Analyser/nsrt/gmp-operators.php | 22 +++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 44811236ad..5e5336da67 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2612,8 +2612,19 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type $type = $getTypeCallback($expr); // GMP supports unary minus and returns GMP - if ($type->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($type)->yes()) { - return new ObjectType('GMP'); + $gmpType = new ObjectType('GMP'); + if ($gmpType->isSuperTypeOf($type)->yes()) { + return $gmpType; + } + + // Handle union types containing GMP + if ($gmpType->isSuperTypeOf($type)->maybe()) { + $nonGmpType = TypeCombinator::remove($type, $gmpType); + $nonGmpResult = $this->getUnaryMinusTypeFromType($expr, $nonGmpType); + if ($nonGmpResult instanceof IntegerRangeType) { + $nonGmpResult = $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); + } + return TypeCombinator::union($gmpType, $nonGmpResult); } $type = $this->getUnaryMinusTypeFromType($expr, $type); @@ -2658,8 +2669,16 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type $exprType = $getTypeCallback($expr); // GMP supports bitwise not and returns GMP - if ($exprType->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($exprType)->yes()) { - return new ObjectType('GMP'); + $gmpType = new ObjectType('GMP'); + if ($gmpType->isSuperTypeOf($exprType)->yes()) { + return $gmpType; + } + + // Handle union types containing GMP + if ($gmpType->isSuperTypeOf($exprType)->maybe()) { + $nonGmpType = TypeCombinator::remove($exprType, $gmpType); + $nonGmpResult = $this->getBitwiseNotTypeFromType($nonGmpType); + return TypeCombinator::union($gmpType, $nonGmpResult); } return $this->getBitwiseNotTypeFromType($exprType); diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php index 5c7d4e5fed..d99a9a500a 100644 --- a/tests/PHPStan/Analyser/nsrt/gmp-operators.php +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -191,3 +191,25 @@ function gmpWithNumericString(\GMP $a, string $s): void assertType('GMP', gmp_add($a, '123')); assertType('GMP', gmp_mul($a, '456')); } + +// ============================================================================= +// Union types - unary operators handle GMP in unions correctly +// ============================================================================= + +/** + * @param \GMP|int $maybeGmp + */ +function gmpUnionUnaryMinus($maybeGmp): void +{ + // Unary minus on GMP|int returns GMP (from GMP branch) | int (from int branch) + assertType('GMP|int', -$maybeGmp); +} + +/** + * @param \GMP|int $maybeGmp + */ +function gmpUnionBitwiseNot($maybeGmp): void +{ + // Bitwise NOT on GMP|int returns GMP (from GMP branch) | int (from int branch) + assertType('GMP|int', ~$maybeGmp); +}