From de6634ff69903bca04144721255812f0fb10e6e3 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 11:32:51 -0700 Subject: [PATCH 1/3] 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 c75bac65a80020697b0863df41888657b981bbbe Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 12:00:02 -0700 Subject: [PATCH 2/3] 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 --- .../InitializerExprTypeResolver.php | 45 +++++++++++ .../GmpOperatorTypeSpecifyingExtension.php | 74 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/gmp-operators.php | 7 +- 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/Type/Php/GmpOperatorTypeSpecifyingExtension.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 0891f487b7..67c479f138 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -986,6 +986,14 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseAnd($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + return $this->getBitwiseAndTypeFromTypes($leftType, $rightType); } @@ -1044,6 +1052,14 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseOr($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + return $this->getBitwiseOrTypeFromTypes($leftType, $rightType); } @@ -1092,6 +1108,14 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseXor($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + return $this->getBitwiseXorTypeFromTypes($leftType, $rightType); } @@ -2034,6 +2058,17 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { + // Check operator type specifying extensions first for object types + // This allows extensions like GmpOperatorTypeSpecifyingExtension to + // handle operator overloading before integer range optimizations kick in + if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + $types = TypeCombinator::union($leftType, $rightType); $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); @@ -2581,6 +2616,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))); @@ -2622,6 +2662,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); } 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 ade4b496709ecb9be02c16cebfb97fdad90c9062 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 12:42:12 -0700 Subject: [PATCH 3/3] Address review feedback: remove object condition guards - Make extension calls unconditional in getBitwiseAndType, getBitwiseOrType, getBitwiseXorType per reviewer feedback - Move extension call to top of resolveCommonMath and remove duplicate later call Co-Authored-By: Claude Opus 4.5 --- .../InitializerExprTypeResolver.php | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 67c479f138..3f73892293 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -986,12 +986,10 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { - $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() - ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseAnd($left, $right), $leftType, $rightType); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseAnd($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; } return $this->getBitwiseAndTypeFromTypes($leftType, $rightType); @@ -1052,12 +1050,10 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { - $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() - ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseOr($left, $right), $leftType, $rightType); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseOr($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; } return $this->getBitwiseOrTypeFromTypes($leftType, $rightType); @@ -1108,12 +1104,10 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { - $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() - ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseXor($left, $right), $leftType, $rightType); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseXor($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; } return $this->getBitwiseXorTypeFromTypes($leftType, $rightType); @@ -2058,15 +2052,10 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { - // Check operator type specifying extensions first for object types - // This allows extensions like GmpOperatorTypeSpecifyingExtension to - // handle operator overloading before integer range optimizations kick in - if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) { - $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() - ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; } $types = TypeCombinator::union($leftType, $rightType); @@ -2108,12 +2097,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri } } - $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() - ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } - if ( $leftType->isArray()->yes() || $rightType->isArray()->yes()