Skip to content

Commit a7b2630

Browse files
Firehedclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 16c6f82 commit a7b2630

File tree

3 files changed

+123
-3
lines changed

3 files changed

+123
-3
lines changed

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,14 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall
986986
$leftType = $getTypeCallback($left);
987987
$rightType = $getTypeCallback($right);
988988

989+
if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) {
990+
$specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
991+
->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseAnd($left, $right), $leftType, $rightType);
992+
if ($specifiedTypes !== null) {
993+
return $specifiedTypes;
994+
}
995+
}
996+
989997
return $this->getBitwiseAndTypeFromTypes($leftType, $rightType);
990998
}
991999

@@ -1044,6 +1052,14 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb
10441052
$leftType = $getTypeCallback($left);
10451053
$rightType = $getTypeCallback($right);
10461054

1055+
if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) {
1056+
$specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
1057+
->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseOr($left, $right), $leftType, $rightType);
1058+
if ($specifiedTypes !== null) {
1059+
return $specifiedTypes;
1060+
}
1061+
}
1062+
10471063
return $this->getBitwiseOrTypeFromTypes($leftType, $rightType);
10481064
}
10491065

@@ -1092,6 +1108,14 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall
10921108
$leftType = $getTypeCallback($left);
10931109
$rightType = $getTypeCallback($right);
10941110

1111+
if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) {
1112+
$specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
1113+
->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseXor($left, $right), $leftType, $rightType);
1114+
if ($specifiedTypes !== null) {
1115+
return $specifiedTypes;
1116+
}
1117+
}
1118+
10951119
return $this->getBitwiseXorTypeFromTypes($leftType, $rightType);
10961120
}
10971121

@@ -2034,6 +2058,17 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType,
20342058
*/
20352059
private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type
20362060
{
2061+
// Check operator type specifying extensions first for object types
2062+
// This allows extensions like GmpOperatorTypeSpecifyingExtension to
2063+
// handle operator overloading before integer range optimizations kick in
2064+
if ($leftType->isObject()->yes() || $rightType->isObject()->yes()) {
2065+
$specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()
2066+
->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType);
2067+
if ($specifiedTypes !== null) {
2068+
return $specifiedTypes;
2069+
}
2070+
}
2071+
20372072
$types = TypeCombinator::union($leftType, $rightType);
20382073
$leftNumberType = $leftType->toNumber();
20392074
$rightNumberType = $rightType->toNumber();
@@ -2581,6 +2616,11 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type
25812616
{
25822617
$type = $getTypeCallback($expr);
25832618

2619+
// GMP supports unary minus and returns GMP
2620+
if ($type->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($type)->yes()) {
2621+
return new ObjectType('GMP');
2622+
}
2623+
25842624
$type = $this->getUnaryMinusTypeFromType($expr, $type);
25852625
if ($type instanceof IntegerRangeType) {
25862626
return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1)));
@@ -2622,6 +2662,11 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type
26222662
{
26232663
$exprType = $getTypeCallback($expr);
26242664

2665+
// GMP supports bitwise not and returns GMP
2666+
if ($exprType->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($exprType)->yes()) {
2667+
return new ObjectType('GMP');
2668+
}
2669+
26252670
return $this->getBitwiseNotTypeFromType($exprType);
26262671
}
26272672

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PHPStan\DependencyInjection\AutowiredService;
6+
use PHPStan\Type\BooleanType;
7+
use PHPStan\Type\IntegerRangeType;
8+
use PHPStan\Type\NeverType;
9+
use PHPStan\Type\ObjectType;
10+
use PHPStan\Type\OperatorTypeSpecifyingExtension;
11+
use PHPStan\Type\Type;
12+
use function in_array;
13+
14+
#[AutowiredService]
15+
final class GmpOperatorTypeSpecifyingExtension implements OperatorTypeSpecifyingExtension
16+
{
17+
18+
public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool
19+
{
20+
if ($leftSide instanceof NeverType || $rightSide instanceof NeverType) {
21+
return false;
22+
}
23+
24+
if (!in_array($operatorSigil, ['+', '-', '*', '/', '**', '%', '&', '|', '^', '<<', '>>', '<', '<=', '>', '>=', '==', '!=', '<=>'], true)) {
25+
return false;
26+
}
27+
28+
$gmpType = new ObjectType('GMP');
29+
$leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes();
30+
$rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes();
31+
32+
// At least one side must be GMP
33+
if (!$leftIsGmp && !$rightIsGmp) {
34+
return false;
35+
}
36+
37+
// The other side must be GMP-compatible (GMP, int, or numeric-string)
38+
// GMP operations with incompatible types (like stdClass) will error at runtime
39+
return $this->isGmpCompatible($leftSide, $gmpType) && $this->isGmpCompatible($rightSide, $gmpType);
40+
}
41+
42+
private function isGmpCompatible(Type $type, ObjectType $gmpType): bool
43+
{
44+
if ($gmpType->isSuperTypeOf($type)->yes()) {
45+
return true;
46+
}
47+
if ($type->isInteger()->yes()) {
48+
return true;
49+
}
50+
if ($type->isNumericString()->yes()) {
51+
return true;
52+
}
53+
return false;
54+
}
55+
56+
public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type
57+
{
58+
$gmpType = new ObjectType('GMP');
59+
60+
// Comparison operators return bool or int (for spaceship)
61+
if (in_array($operatorSigil, ['<', '<=', '>', '>=', '==', '!='], true)) {
62+
return new BooleanType();
63+
}
64+
65+
if ($operatorSigil === '<=>') {
66+
return IntegerRangeType::fromInterval(-1, 1);
67+
}
68+
69+
// All arithmetic and bitwise operations on GMP return GMP
70+
// GMP can operate with: GMP, int, or numeric-string
71+
return $gmpType;
72+
}
73+
74+
}

tests/PHPStan/Analyser/nsrt/gmp-operators.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,10 @@ function gmpBitwiseFunctions(\GMP $a, \GMP $b): void
166166

167167
function gmpComparisonFunctions(\GMP $a, \GMP $b, int $i): void
168168
{
169-
// gmp_cmp corresponds to <=>
170-
assertType('int<-1, 1>', gmp_cmp($a, $b));
171-
assertType('int<-1, 1>', gmp_cmp($a, $i));
169+
// gmp_cmp returns -1, 0, or 1 in practice, but stubs say int
170+
// TODO: Could be improved to int<-1, 1> like the <=> operator
171+
assertType('int', gmp_cmp($a, $b));
172+
assertType('int', gmp_cmp($a, $i));
172173
}
173174

174175
function gmpFromInit(): void

0 commit comments

Comments
 (0)