Skip to content

Compute operators and loose comparison on value sets - #6957

Merged
ondrejmirtes merged 15 commits into
2.3.xfrom
loose-comparison-sets
Nov 5, 2026
Merged

ondrejmirtes merged 15 commits into
2.3.xfrom
loose-comparison-sets

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Moves the remaining operator and loose comparison logic out of OperatorTypeHelper and LooseComparisonHelper into polymorphic value sets, and deletes both helpers (and ExponentiateHelper).

  • Type::toValueSet() on every type describes the values it holds: null/false/true, numbers (NumberSet), strings (StringSet), arrays, objects, resources, plus unknown/benevolent/invalid flags. UnionType unions member sets, IntersectionType intersects them.
  • Loose equality (getPossiblyLooselyEqual, getAlwaysLooselyEqual, looseCompare) is computed on ValueSet. Constant folding of == and <=> is ValueSet::compareScalarValues(), with the versions to compare under from PhpVersions::getLooseComparisonVersions().
  • %, intdiv, & | ^, negation and ** are on NumberSet, alongside + - * / << >>. Operand handling (arrays/objects error, never absorbs, mixed gives benevolent int|float) is on ValueSet.
  • Concatenation is on StringSet, which tracks constant strings, a type for the rest, and digits / whitespaceFree flags that decide whether numeric-string survives.
  • The array + lives in OperatorTypeTrait.

ValueSet, NumberSet and StringSet are shadowed by native Turbo mirrors in the same commits.

Behaviour changes (concatenation only):

  • a never operand in . gives never (bcmath-number.php line 387, two write-only-property errors gone from TypesAssignedToPropertiesRuleTest);
  • decimal-int-string . <float or numeric> and int|null . <digits> keep numeric-string.

The other commits are pure refactors: a dump of all operator and loose-equality results over a catalog of ~120 types on PHP 7.4/8.0/8.4 is byte-identical before and after each of them, with Turbo on and off (the only earlier difference: never possibly-equals never instead of mixed).

Verified: full test suite with and without Turbo, self-analysis identical with Turbo on/off, operator fuzzer on all 11 PHP versions with Turbo on/off, side-by-side.php, smoke.php, signature-parity.php, make lint-turbo.

Closes phpstan/phpstan#15388

…er and strings

The always-equal values of a scalar no longer come from testing a fixed
list of candidate values. null and the booleans follow its truthiness,
the numbers its number, and the strings are its own, its number's and
the falsy ones, printed independently of the precision ini settings.

The numbers that can equal a number type are read from its
toNumberSet() instead of instanceof checks on the integer and float
types.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
Every type now describes its values as a ValueSet: null, the booleans,
a NumberSet of numbers, constant strings and the other strings,
arrays by emptiness, objects and resources. A union of types is the
union of their sets, an intersection their intersection.

getPossiblyLooselyEqualType() and getAlwaysLooselyEqualType() are
computed on the ValueSet, so the per-type dispatch in
LooseComparisonHelper is gone; it keeps the comparison of two values
and the PHP version helpers.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
The operand handling of +, -, *, /, intdiv, %, **, negation and the
bitwise operators moves from OperatorTypeHelper and ExponentiateHelper
onto ValueSet and NumberSet. Types describe themselves through
toValueSet() (invalid, unknown, never, arrays, benevolent) and the
OperatorTypeTrait methods delegate to it. ExponentiateHelper is removed;
OperatorTypeHelper keeps only concatenation and array union.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
The value set every Type::toValueSet() returns, with the arithmetic,
bitwise and loose-equality computations on it, is implemented natively
in ValueSet.cpp. LooseComparisonHelper stays PHP and is called from it.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
ValueSet keeps its strings as a StringSet: the constant strings plus the
type of the other strings. The . operator concatenates the strings the
values cast to, and the strings of numbers keep what their type cannot
say: they never end in whitespace, and those of non-negative integers
are digits only. A numeric left side thus stays numeric after digits
whatever produced them, and after a decimal integer string a fraction
or exponent keeps it numeric. A never operand gives never, as it does
for the arithmetic operators.

The native mirror is StringSet.cpp.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
OperatorTypeTrait::arrayUnion() unions the arrays itself, the last
operator OperatorTypeHelper computed, which is removed. Its native body
moves to OperatorTypeTrait.cpp.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
PhpVersions::getLooseComparisonVersions() lists the versions whose loose
comparison semantics differ; numeric string parsing moves to StringSet.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
The main process peaks at 428 MB on 2.3.x and already runs out of the 450M limit on Windows.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
The PHP 7.4 downgrade does not transform ?->, and PHP 8.5 warns when casting NAN to bool.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
Handlers passed to reg::Class::method() need ZEND_FASTCALL, which is __vectorcall on Windows, and MSVC has no __builtin_*_overflow.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
AcceptsResult: :or() takes a single operand. Spreading every member into it dropped all but the first in PHP, and the native AcceptsResult::or() rejected the call when Turbo trusts PHPStan's own types.
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
zend_is_true() warns on NAN on PHP 8.5, like the (bool) cast in PHP.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
A benevolent array<string>|string, such as str_replace() of mixed, concatenates as its strings again, as Type::toString() of a BenevolentUnionType leaves out what cannot be cast.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
@ondrejmirtes
ondrejmirtes merged commit 7a343dd into 2.3.x Nov 5, 2026
543 checks passed
@ondrejmirtes
ondrejmirtes deleted the loose-comparison-sets branch November 5, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2.3.0: array_reduce returning int now returns float|int if part of the result is a function

1 participant