Repository navigation
Move operator type inference to Type methods - #6930
Merged
Merged
Conversation
… methods Each operator is now a Type method taking the right operand and the configured PhpVersion: arithmeticAdd(), arithmeticSubtract(), arithmeticMultiply(), arithmeticDivide(), arithmeticIntDivide(), arithmeticModulo(), arithmeticPower(), arithmeticNegate(), bitwiseAnd(), bitwiseOr(), bitwiseXor(), bitwiseShiftLeft(), bitwiseShiftRight(), stringConcatenate() and arrayUnion(). Their shared bodies live in OperatorTypeHelper (shadowed natively) through OperatorTypeTrait. exponentiate() is deprecated in favour of arithmeticPower(). InitializerExprTypeResolver calls the operator type-specifying extensions first and the Type method second, so an extension now also sees operations the resolver used to shortcut (BcMath\Number * null is an error now instead of 0). The inferred types are otherwise unchanged: they are byte-identical on every supported PHP version, with and without the turbo extension. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
%, <<, >>, &, | and ^ converted a constant numeric string through the float of toNumber(), so '9223372036854775808' wrapped around instead of saturating like PHP's (int) conversion does, and a shift by it crashed the analysis with ArithmeticError. The operands now come from toInteger(). Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
Zero times a float is 0.0, -0.0 or NAN (0 * INF), so 0 * $float is float now instead of 0.0, and zero times a type that is not certainly an integer (numeric-string, int|float) is no longer the integer 0. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
The bounds of base ** exponent were the powers of the bounds, which holds only for bases of at least 1: 0 ** int<0, 10> was never instead of 0|1, (-1) ** int<0, 10> was 1 instead of -1|1, and an int<2, 3> base raised to int<min, 3> missed the floats of negative exponents. ExponentiateHelper now handles integer intervals for every base, IntegerRangeType inherits it from IntegerType and null is exponentiated as 0 instead of always 0|1. A float operand makes the result a float, the power of 0 is 1|1.0 unless the base is an integer, and a non-numeric constant string operand is an error. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
An integer followed by ' 1' or '+5' is not numeric although those strings are. The appended string now has to be digits with an optional fraction and exponent, and neither check accepts a trailing newline, which PHP 7 does not allow in numeric strings. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
A constant minus a range unbounded on either side was int<min, constant>, which is wrong for a negative operand (0 - int<min, -1> is int<1, max>, not int<min, 0>), and the other cases took the bounds from mismatched ends before swapping them. [a, b] - [c, d] is [a - d, b - c]. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
Only two strings make a string; with an int, a float, a bool or null on the other side the string is a number, so '1' & $mixed is 1 for $mixed = 1. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
… strings int|null . bool is '' for null . false, so it is not numeric although removing '' from the left side leaves a numeric string. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
… PHP 8
PHP 8 compares an integer with a non-numeric string as strings, which does
not keep the order of the integers ('10' < '1abc' < '2'), so the bounds of
a range do not decide the comparison: int<0, 10> < '1abc' was true although
2 < '1abc' is false. Each integer of a finite range decides it now, and an
infinite range is maybe.
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
…whitespace PHP 8 allows trailing whitespace in numeric strings, so '1 ' is numeric but '1 ' . '5' is not. Only an integer, a decimal-int-string or constant strings not ending in whitespace (and any numeric-string on PHP 7) keep the result numeric. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
A member's ErrorType is a MixedType, so intersecting it with the other members' powers hid it: numeric-string ** 'abc' was float|int although it always throws a TypeError. An erroring member now decides for the intersection. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
…in the config PHP 7.4 reads the class attribute as a comment, so the class starts a line later there and the inline ignore above the attribute matched nothing. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
ondrejmirtes
force-pushed
the
operator-type-methods
branch
from
November 3, 2026 22:17
1e63163 to
d9fc04e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every operator
InitializerExprTypeResolverhandled is now a method onTypethat takes the right operand and the configuredPhpVersion:arithmeticAdd,arithmeticSubtract,arithmeticMultiply,arithmeticDivide,arithmeticIntDivide,arithmeticModulo,arithmeticPower,arithmeticNegatebitwiseAnd,bitwiseOr,bitwiseXor,bitwiseShiftLeft,bitwiseShiftRightstringConcatenate,arrayUnionexponentiate()is deprecated in favour ofarithmeticPower(). The logic lives inOperatorTypeHelper, whichOperatorTypeTraitdelegates to. Both have native mirrors in turbo-ext. The resolver still runs the operator type-specifying extensions first (GMP, BcMath).Constant scalar comparisons no longer run
==/<at analysis time, so they follow the configured PHP version instead of the runtime one (LooseComparisonHelper::compareValues()).The move commit doesn't change inferred types. The only exception is
BcMath\Number * null, which is now an error instead of0. The commits after it are soundness fixes, each with an nsrt test that fails without it:0 * $floatisfloat, because0 * INFisNAN.0 ** int<0, 10>,(-1) ** int<0, 10>, negative exponents.$int . $stringstays numeric only when the string is digits.$string & $mixedisint|string.numeric-stringfor a concatenation of two possibly empty strings.numeric-stringonly when the left side can't end in whitespace, as an int, a float or a decimal-int-string can't ('1 'is numeric,'1 ' . '5'isn't).$numericString ** 'abc'is an error.Verified with a local differential fuzzer that compares inferred types with the values the operators produce on PHP 7.1–8.6. It runs with turbo on and off and is not part of this PR. The full test suite passes with and without turbo. Self-analysis output is byte-identical with turbo on and off.
side-by-side.php,smoke.php,signature-parity.phpandmake lint-turbopass.Closes phpstan/phpstan#11732