Skip to content

Add FloatRangeType for float<min, max> - #6937

Open
ondrejmirtes wants to merge 7 commits into
2.3.xfrom
float-range-type
Open

ondrejmirtes wants to merge 7 commits into
2.3.xfrom
float-range-type

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Adds FloatRangeType, so PHPStan can say which floats a value can be, not only that it is a float.

Syntax. float<min, max>, with min and max for open ends, plus positive-float, non-negative-float, negative-float, non-positive-float and finite-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 both 0.0 and -0.0.

What uses it

  • Comparisons narrow floats into ranges: $f >= 0 keeps float<0.0, max>. The true branch of a comparison drops NAN.
  • is_nan(), is_finite() and is_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 as INF - INF or 0 * INF.
  • Casts:
    • (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) and hrtime(true) return ranges. Int division gets a float range part, so ceil(count($list) / 4) is float<0.0, max>.

Fixes along the way

  • (string) $float is no longer numeric-string, because 'INF', '-INF' and 'NAN' are not numeric.
  • A zero float is folded as either zero wherever the sign shows. For example, $f ** -1 after $f === 0.0 can be INF or -INF.
  • Unary minus negates every member of a union.

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

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
…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
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.

Support for signed float types Add range for float

1 participant