Repository navigation
Add FloatRangeType for float<min, max> - #6937
Open
ondrejmirtes wants to merge 7 commits into
Open
ondrejmirtes wants to merge 7 commits into
ondrejmirtes wants to merge 7 commits into
Conversation
0.0 === -0.0, so PHPStan treats the two zeros as one value, like
ConstantFloatType::toString() already does. Constant folding still used
the sign of the literal: after $f === 0.0, $f ** -1 was INF and
sprintf('%s', $f) and implode() were '0', although -0.0 gives -INF and
'-0'. They now include the other zero's result.
Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
-(int<1, 5>|1.5) kept the operand's type: only a union of constants and a lone integer range were negated, any other number type came back unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
A float range is every double between its closed bounds in PHP's comparison order, NAN excluded. The two zeros are one point of that order, so a zero bound always takes both of them. The PHPDoc syntax is float<a, b> with min and max, plus positive-float, non-negative-float, negative-float, non-positive-float and finite-float. Union, intersection and removal are exact: the bounds map to integers that keep the order, so the next double is the next integer. Negation mirrors the bounds. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
ondrejmirtes
force-pushed
the
float-range-type
branch
from
November 4, 2026 08:50
4861e95 to
b3bab80
Compare
…s_infinite() A comparison against a number now narrows a float to the range on its side: $f >= 0 keeps float<0.0, max>. NAN compares false against every number, so the true branch of a comparison drops it and the false branch keeps it. The old 0.0 stand-ins for a range are gone, which also stops 0.0 > -1 from dropping 0.0. Removing NAN, an infinity or a range from float leaves the rest of float<min, max> and NAN; a finite constant would split it, so float !== 1.5 stays float. A full range and NAN union back into float. is_nan(), is_finite() and is_infinite() narrow their argument and fold to true or false when it decides them. (float) int<a, b> is float<(float) a, (float) b>. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
INF, -INF and NAN stringify as 'INF', '-INF' and 'NAN', which are not numeric, so (string) $float is non-empty-string&uppercase-string. A finite float refinement can restore numeric-string where finiteness is proven. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
+, -, * and / with an operand that holds a float range pair up the members of both operands: two integers keep the integer operators, any other pair is computed at the corners of its intervals, where rounded IEEE operations are monotonic. NAN is added where a pair can yield it (INF - INF, 0 * INF, INF / INF, 0 / 0 on PHP 7), and a NAN corner is bounded by the floats next to it. On PHP 8 zero divisors throw, so they are left out of the divisor. This also stops a float member of a union operand from passing through unchanged, which made (float<1, 2>|int<2, 3>) + 1 keep float<1, 2>. (string) of a finite range is a numeric-string, of one without the zeros a non-falsy-string; (int) of a range inside the int range is an int range. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
ceil() and floor() map the bounds of a range. round() is bounded by floor() and ceil() of them, since PHP versions and modes round .5 differently, and keeps only the sign for another precision. sqrt() maps the bounds and adds NAN below zero; fmod() has the sign of the dividend and a magnitude below both operands, NAN for an infinite dividend or a zero divisor. lcg_value() is float<0, 1>, microtime(true) non-negative, and the float of hrtime(true) at least 1. The float part of an int division is the range of the quotients of the bounds unless it is every float but NAN, so count($list) / 4 is float<0.0, max>|int<0, max> and its ceil() float<0.0, max>. An integer divisor next to zero is -1 or 1, not a denormal, and the infinities that stand for an unbounded int no longer make NAN. Closes phpstan/phpstan#6963 Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
ondrejmirtes
force-pushed
the
float-range-type
branch
from
November 4, 2026 10:29
b3bab80 to
41b8214
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.
Adds
FloatRangeType, so PHPStan can say which floats a value can be, not only that it is a float.Syntax.
float<min, max>, withminandmaxfor open ends, pluspositive-float,non-negative-float,negative-float,non-positive-floatandfinite-float. Bounds are closed. A range holds every double between its bounds in PHP's comparison order. NAN is never in a range; ±INF can be. The two zeros are one point in that order, so a zero bound always includes both0.0and-0.0.What uses it
$f >= 0keepsfloat<0.0, max>. The true branch of a comparison drops NAN.is_nan(),is_finite()andis_infinite()narrow their argument, and fold to true or false when the type decides them.+,-,*and/compute ranges at the interval corners. NAN is added where an operand pair can produce it, such asINF - INFor0 * INF.(float) int<a, b>gives a float range.(int)of a range inside the int range gives an int range.(string)of a finite range gives a numeric-string.ceil(),floor(),round(),sqrt(),fmod(),lcg_value(),microtime(true)andhrtime(true)return ranges. Int division gets a float range part, soceil(count($list) / 4)isfloat<0.0, max>.Fixes along the way
(string) $floatis no longernumeric-string, because'INF','-INF'and'NAN'are not numeric.$f ** -1after$f === 0.0can beINFor-INF.Every new type and handler change has its Turbo mirror. Self-analysis is byte-identical with Turbo on and off. The local operator fuzzer passes on PHP 7.1 through 8.6, with and without Turbo.
Closes phpstan/phpstan#6963
Closes phpstan/phpstan#13859