Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '63cd54c';
public const EXPECTED_EXTENSION_VERSION = '22b361a';

private static bool $active = false;

Expand Down
21 changes: 13 additions & 8 deletions src/Type/IntegerRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
class IntegerRangeType extends IntegerType implements CompoundType
{

/** The first float above PHP_INT_MAX: 2^63 on 64-bit builds, 2^31 on 32-bit ones, exact on both. */
private const FLOAT_ABOVE_INT_MAX = PHP_INT_MAX + 1.0;

private function __construct(private ?int $min, private ?int $max)
{
parent::__construct();
Expand Down Expand Up @@ -92,17 +95,19 @@ public static function createAllSmallerThan($value): Type
return self::fromInterval(null, $value, -1);
}

// a float never holds PHP_INT_MAX itself, so the first float past the
// int range is (float) PHP_INT_MAX: every int is smaller than that
if ($value >= PHP_INT_MAX) {
// decided on the ceil() about to be cast: comparing $value with PHP_INT_MAX
// converts PHP_INT_MAX to float, which rounds it up to 2^63 on 64-bit builds
// but keeps it exact on 32-bit ones, where 2147483647.0 is still in range
$ceil = ceil($value);
if ($ceil >= self::FLOAT_ABOVE_INT_MAX) {
return new IntegerType();
}

if ($value <= PHP_INT_MIN) {
return new NeverType();
}

return self::fromInterval(null, (int) ceil($value), -1);
return self::fromInterval(null, (int) $ceil, -1);
}

/**
Expand Down Expand Up @@ -164,13 +169,13 @@ public static function createAllGreaterThanOrEqualTo($value): Type
return new IntegerType();
}

// (float) PHP_INT_MAX is already past the int range, so no int
// reaches it
if ($value >= PHP_INT_MAX) {
// decided on the ceil() about to be cast, like createAllSmallerThan()
$ceil = ceil($value);
if ($ceil >= self::FLOAT_ABOVE_INT_MAX) {
return new NeverType();
}

return self::fromInterval((int) ceil($value), null);
return self::fromInterval((int) $ceil, null);
}

public function getMin(): ?int
Expand Down
66 changes: 57 additions & 9 deletions tests/PHPStan/Type/IntegerRangeTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,69 @@
namespace PHPStan\Type;

use PHPStan\Testing\PHPStanTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use RuntimeException;
use function restore_error_handler;
use function set_error_handler;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
use const PHP_INT_SIZE;

class IntegerRangeTypeTest extends PHPStanTestCase
{

public function testCreateFromFloatPastTheIntRange(): void
public static function dataFloatBounds(): iterable
{
// PHP_INT_MAX is not representable as a float: (float) PHP_INT_MAX is
// the first float past the int range
$pastMax = (float) PHP_INT_MAX;

$this->assertSame('int', IntegerRangeType::createAllSmallerThan($pastMax)->describe(VerbosityLevel::precise()));
$this->assertSame('int', IntegerRangeType::createAllSmallerThanOrEqualTo($pastMax)->describe(VerbosityLevel::precise()));
$this->assertSame('*NEVER*', IntegerRangeType::createAllGreaterThan($pastMax)->describe(VerbosityLevel::precise()));
$this->assertSame('*NEVER*', IntegerRangeType::createAllGreaterThanOrEqualTo($pastMax)->describe(VerbosityLevel::precise()));
$smallerThan = static fn (float $value): Type => IntegerRangeType::createAllSmallerThan($value);
$smallerThanOrEqualTo = static fn (float $value): Type => IntegerRangeType::createAllSmallerThanOrEqualTo($value);
$greaterThan = static fn (float $value): Type => IntegerRangeType::createAllGreaterThan($value);
$greaterThanOrEqualTo = static fn (float $value): Type => IntegerRangeType::createAllGreaterThanOrEqualTo($value);

// PHP_INT_MAX + 1.0 is the first float above PHP_INT_MAX on any int width, and
// (float) PHP_INT_MIN is exact on any int width
$aboveMax = PHP_INT_MAX + 1.0;
yield [$smallerThan, $aboveMax, 'int'];
yield [$smallerThanOrEqualTo, $aboveMax, 'int'];
yield [$greaterThan, $aboveMax, '*NEVER*'];
yield [$greaterThanOrEqualTo, $aboveMax, '*NEVER*'];
yield [$smallerThan, (float) PHP_INT_MIN, '*NEVER*'];
yield [$smallerThanOrEqualTo, (float) PHP_INT_MIN, (string) PHP_INT_MIN];
yield [$greaterThan, (float) PHP_INT_MIN, 'int<' . (PHP_INT_MIN + 1) . ', max>'];
yield [$greaterThanOrEqualTo, (float) PHP_INT_MIN, 'int'];

// (float) PHP_INT_MAX is 2^63 on 64-bit builds (above every int) and exact on 32-bit
// ones; so is PHP_INT_MAX - 0.5, which is PHP_INT_MAX - 1 + 0.5 on 32-bit builds
$maxIsExact = PHP_INT_SIZE < 8;
yield [$smallerThan, (float) PHP_INT_MAX, $maxIsExact ? 'int<min, ' . (PHP_INT_MAX - 1) . '>' : 'int'];
yield [$smallerThanOrEqualTo, (float) PHP_INT_MAX, 'int'];
yield [$greaterThan, (float) PHP_INT_MAX, '*NEVER*'];
yield [$greaterThanOrEqualTo, (float) PHP_INT_MAX, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*'];
yield [$smallerThan, PHP_INT_MAX - 0.5, $maxIsExact ? 'int<min, ' . (PHP_INT_MAX - 1) . '>' : 'int'];
yield [$greaterThanOrEqualTo, PHP_INT_MAX - 0.5, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*'];

// between PHP_INT_MAX and the next int on 32-bit builds, 2^63 on 64-bit ones: its
// ceil() is past the int range either way
yield [$smallerThan, PHP_INT_MAX + 0.5, 'int'];
yield [$greaterThanOrEqualTo, PHP_INT_MAX + 0.5, '*NEVER*'];

yield [$smallerThan, 2.5, 'int<min, 2>'];
yield [$greaterThan, 2.5, 'int<3, max>'];
}

/**
* @param callable(float): Type $factory
*/
#[DataProvider('dataFloatBounds')]
public function testFloatBounds(callable $factory, float $value, string $expected): void
{
set_error_handler(static function (int $errno, string $errstr): bool {
throw new RuntimeException($errstr);
});
try {
$this->assertSame($expected, $factory($value)->describe(VerbosityLevel::precise()));
} finally {
restore_error_handler();
}
}

}
84 changes: 59 additions & 25 deletions turbo-ext/src/IntegerRangeType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ namespace sigs = ptdecl::IntegerRangeType::sig;

zend_class_entry *pt_ce_integer_range_type = nullptr;

/* the twin's private const FLOAT_ABOVE_INT_MAX = PHP_INT_MAX + 1.0: the first
* double above ZEND_LONG_MAX, exact on 64-bit (2^63) and 32-bit (2^31) builds */
static constexpr double PT_IRT_FLOAT_ABOVE_INT_MAX = (double) ZEND_LONG_MAX + 1.0;

/* nothing is memoized per request since InitializerExprTypeResolver's
* CALCULATE_SCALARS_LIMIT became a native constant */
void pt_integer_range_type_rinit()
Expand Down Expand Up @@ -171,26 +175,26 @@ class IntegerRangeType

/* the createAll*() family: the range of integers beyond an int|float
* $value. An int goes straight to fromInterval(); anything else is
* first held against the int limits — a float the way the VM compares
* int with float (both as doubles), any other value loosely — and what
* passes is then rounded by ceil()/floor() and (int)-cast, which is
* where a non-number fails under strict_types. */
* held against the int limits — a float the way the VM compares int
* with float (both as doubles), any other value loosely — and rounded
* by ceil()/floor() and (int)-cast, which is where a non-number fails
* under strict_types. Where the ceil() is held against the max instead
* of $value (createAllSmallerThan(), createAllGreaterThanOrEqualTo()),
* it is taken first, in the twin's order. */

/* fromInterval(null, $value, -1) */
static zv::Val createAllSmallerThan(zval *value)
{
if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::null(), NullableLong::of(Z_LVAL_P(value)), -1);
int againstMax, againstMin;
if (UNEXPECTED(!compareToLimits(value, againstMax, againstMin))) return zv::Val();
if (againstMax >= 0) { /* $value >= PHP_INT_MAX */
double ceiled;
if (UNEXPECTED(!roundToDouble(value, true, ceiled))) return zv::Val();
if (ceiled >= PT_IRT_FLOAT_ABOVE_INT_MAX) { /* $ceil >= self::FLOAT_ABOVE_INT_MAX */
return integer();
}
if (againstMin <= 0) { /* $value <= PHP_INT_MIN */
if (Z_DVAL_P(value) <= (double) ZEND_LONG_MIN) { /* $value <= PHP_INT_MIN */
return never();
}
zend_long rounded;
if (UNEXPECTED(!roundToLong(value, true, rounded))) return zv::Val();
return fromInterval(NullableLong::null(), NullableLong::of(rounded), -1);
return fromInterval(NullableLong::null(), NullableLong::of(zend_dval_to_lval(ceiled)), -1);
}

/* fromInterval(null, $value) */
Expand Down Expand Up @@ -231,17 +235,17 @@ class IntegerRangeType
static zv::Val createAllGreaterThanOrEqualTo(zval *value)
{
if (Z_TYPE_P(value) == IS_LONG) return fromInterval(NullableLong::of(Z_LVAL_P(value)), NullableLong::null(), 0);
int againstMax, againstMin;
if (UNEXPECTED(!compareToLimits(value, againstMax, againstMin))) return zv::Val();
int againstMin;
if (UNEXPECTED(!compareToMin(value, againstMin))) return zv::Val();
if (againstMin <= 0) { /* $value <= PHP_INT_MIN */
return integer();
}
if (againstMax >= 0) { /* $value >= PHP_INT_MAX */
double ceiled;
if (UNEXPECTED(!roundToDouble(value, true, ceiled))) return zv::Val();
if (ceiled >= PT_IRT_FLOAT_ABOVE_INT_MAX) { /* $ceil >= self::FLOAT_ABOVE_INT_MAX */
return never();
}
zend_long rounded;
if (UNEXPECTED(!roundToLong(value, true, rounded))) return zv::Val();
return fromInterval(NullableLong::of(rounded), NullableLong::null(), 0);
return fromInterval(NullableLong::of(zend_dval_to_lval(ceiled)), NullableLong::null(), 0);
}

/* $this->min / $this->max; false with an Error pending when the
Expand Down Expand Up @@ -1127,34 +1131,58 @@ class IntegerRangeType
* every one of the four tests false); anything else compares loosely.
* false = pending exception */
[[nodiscard]] static bool compareToLimits(zval *value, int &againstMax, int &againstMin)
{
return compareToMax(value, againstMax) && compareToMin(value, againstMin);
}

[[nodiscard]] static bool compareToMax(zval *value, int &againstMax)
{
if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) {
double d = Z_DVAL_P(value);
double max = (double) ZEND_LONG_MAX;
double min = (double) ZEND_LONG_MIN;
againstMax = d > max ? 1 : (d < max ? -1 : (d == max ? 0 : -1));
againstMin = d < min ? -1 : (d > min ? 1 : (d == min ? 0 : 1));
return true;
}
zval limit;
ZVAL_LONG(&limit, ZEND_LONG_MAX);
againstMax = zend_compare(value, &limit);
if (UNEXPECTED(EG(exception))) return false;
return !EG(exception);
}

[[nodiscard]] static bool compareToMin(zval *value, int &againstMin)
{
if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) {
double d = Z_DVAL_P(value);
double min = (double) ZEND_LONG_MIN;
againstMin = d < min ? -1 : (d > min ? 1 : (d == min ? 0 : 1));
return true;
}
zval limit;
ZVAL_LONG(&limit, ZEND_LONG_MIN);
againstMin = zend_compare(value, &limit);
return !EG(exception);
}

/* (int) ceil($value) / (int) floor($value) for a non-int $value: a
* float rounds and casts as the engine casts; anything else is what
* ceil()/floor() reject under strict_types. false = pending exception */
[[nodiscard]] static bool roundToLong(zval *value, bool useCeil, zend_long &out)
/* ceil($value) / floor($value) for a non-int $value: a float rounds;
* anything else is what ceil()/floor() reject under strict_types.
* false = pending exception */
[[nodiscard]] static bool roundToDouble(zval *value, bool useCeil, double &out)
{
if (UNEXPECTED(Z_TYPE_P(value) != IS_DOUBLE)) {
zend_type_error("%s(): Argument #1 ($num) must be of type int|float, %s given", useCeil ? "ceil" : "floor", zend_zval_value_name(value));
return false;
}
out = zend_dval_to_lval(useCeil ? ceil(Z_DVAL_P(value)) : floor(Z_DVAL_P(value)));
out = useCeil ? ceil(Z_DVAL_P(value)) : floor(Z_DVAL_P(value));
return true;
}

/* (int) ceil($value) / (int) floor($value) for a non-int $value, cast as
* the engine casts. false = pending exception */
[[nodiscard]] static bool roundToLong(zval *value, bool useCeil, zend_long &out)
{
double rounded;
if (UNEXPECTED(!roundToDouble(value, useCeil, rounded))) return false;
out = zend_dval_to_lval(rounded);
return true;
}

Expand Down Expand Up @@ -1334,13 +1362,19 @@ static void pt_irt_bound(INTERNAL_FUNCTION_PARAMETERS, bool (IntegerRangeType::*
RETURN_LONG(bound.value);
}

static void pt_irt_float_above_int_max_constant(zval *out)
{
ZVAL_DOUBLE(out, PT_IRT_FLOAT_ABOVE_INT_MAX);
}

PT_MINIT_REGISTRATION(pt_register_integer_range_type)
{
reg::Class cls("PHPStan\\Type\\IntegerRangeType");
ptdecl::IntegerRangeType::declareClass(cls);
/* "min" and "max" must stay the first two declared properties
* (slots::min, slots::max) */
ptdecl::IntegerRangeType::declareProperties(cls);
cls.privateClassConstantValue("FLOAT_ABOVE_INT_MAX", pt_irt_float_above_int_max_constant);

cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) {
zend_long min, max;
Expand Down
4 changes: 2 additions & 2 deletions turbo-ext/tests/type-family.php
Original file line number Diff line number Diff line change
Expand Up @@ -353,13 +353,13 @@
}
}
}
foreach ([0, 1, -1, 42, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MAX - 1, PHP_INT_MIN + 1, 0.0, 0.5, -0.5, 2.5, -2.5, 1e30, -1e30, 9.2233720368547758E+18, -9.2233720368547758E+18, 9.2e18, -9.2e18, (float) PHP_INT_MAX, (float) PHP_INT_MIN, NAN, INF, -INF] as $value) {
foreach ([0, 1, -1, 42, PHP_INT_MAX, PHP_INT_MIN, PHP_INT_MAX - 1, PHP_INT_MIN + 1, 0.0, 0.5, -0.5, 2.5, -2.5, 1e30, -1e30, 9.2233720368547758E+18, -9.2233720368547758E+18, 9.2e18, -9.2e18, (float) PHP_INT_MAX, (float) PHP_INT_MIN, PHP_INT_MAX + 1.0, 9223372036854774784.0, 2147483647.0, 2147483646.5, 2147483647.5, -2147483648.0, -2147483648.5, NAN, INF, -INF] as $value) {
$key = is_float($value) ? var_export($value, true) : (string) $value;
foreach (['createAllSmallerThan', 'createAllSmallerThanOrEqualTo', 'createAllGreaterThan', 'createAllGreaterThanOrEqualTo'] as $factory) {
$r["$factory $key"] = @$view($rangeClass::$factory($value));
}
}
foreach (['5', true, null] as $value) {
foreach (['5', '1e30', '-1e30', true, null] as $value) {
foreach (['createAllSmallerThan', 'createAllGreaterThanOrEqualTo'] as $factory) {
try {
$r["$factory " . var_export($value, true)] = $view($rangeClass::$factory($value));
Expand Down
Loading