Skip to content

Add phpIntSize parameter, infer 64bit from composer.json under bleeding edge#6030

Open
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:php-int-size
Open

Add phpIntSize parameter, infer 64bit from composer.json under bleeding edge#6030
zonuexe wants to merge 2 commits into
phpstan:2.2.xfrom
zonuexe:php-int-size

Conversation

@zonuexe

@zonuexe zonuexe commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Refs phpstan/phpstan#14948

Option (3) from that issue, the first step of the rollout in section 6. I'm opening it so the discussion has code to look at. There's no rush to merge it. If you'd rather go straight to (2) and drop the 32bit branch, say so and I'll close this.

What it does

phpIntSize: 8 pins the three constants to their 64bit values:

parameters:
	phpIntSize: 8
default phpIntSize: 8
PHP_INT_MAX 2147483647|9223372036854775807 9223372036854775807
PHP_INT_MIN -9223372036854775808|-2147483648 -9223372036854775808
PHP_INT_SIZE 4|8 8
PHP_INT_MAX + 1 2147483648|9.223372036854776E+18 9.223372036854776E+18

The last row is the one from the issue: under the default, no PHP build can hold that int(2147483648).

The parameter defaults to null, so nothing changes for anyone who doesn't set it.

Why the schema rejects phpIntSize: 4

The item 'parameters › phpIntSize' expects to be 8|null, 4 given.

Integer literals, overflow to float, and IntegerRangeType's bounds are all fixed at 64bit. Accepting 4 would let a user opt into "PHP_INT_MAX is 2147483647, but 2147483647 + 1 is int(2147483648)", which is the same contradiction the union has today.

Real 32bit semantics could come from either side. Emulate them in the engine, or run PHPStan on a 32bit build of PHP, which gets most of the way there since the engine inherits its integer width from the process it runs in. I doubt either is worth the effort. anyOf(8) leaves room to add 4 if that changes.

Why I pin PHP_INT_SIZE too

Leaving it as 4|8 would keep if (PHP_INT_SIZE === 4) branches alive while PHP_INT_MAX inside them is the 64bit value. Pinned, PHPStan reports identical.alwaysFalse on those branches, which is the answer a project asked for when it opted in. A library that still supports 32bit should not set phpIntSize.

ConstantResolver keeps its existing guard on the host's PHP_INT_SIZE, since PHPStan running on 32bit cannot construct ConstantIntegerType(9223372036854775807).

Bleeding edge: composerPhp64Bit

Composer registers the php-64bit virtual package only when PHP_INT_SIZE === 8 (PlatformRepository.php), and AutoloadGenerator::getPlatformCheck() emits a PHP_INT_SIZE !== 8 assertion into vendor/composer/platform_check.php. A project that requires it is 64bit at install time and at runtime.

So require."php-64bit" in composer.json can imply phpIntSize: 8 with no configuration. But that turns the feature on for projects that never asked, and they would start seeing identical.alwaysFalse on their PHP_INT_SIZE === 4 branches plus different types wherever PHP_INT_MAX flows. I put it behind the composerPhp64Bit feature toggle.

phpIntSize works without the toggle and wins over composer.json.

composer.json {"require": {"php-64bit": "^8.1"}}

                        default      bleeding edge
PHP_VERSION_ID     int<50207, ...>   int<80100, ...>
PHP_INT_MAX        2147483647|...    9223372036854775807

php-64bit also constrains the PHP version

ComposerPhpVersionFactory reads require.php and nothing else, so the project above gets no phpVersion inference today. Composer treats php and php-64bit as constraints on the same version, and takes the highest lower bound across both (AutoloadGenerator::getPlatformCheck() again). ComposerPhpVersionParser::parse() now takes a list of constraints and intersects them with MultiConstraint::create($constraints, true), whose conjunctive bounds are the max of the lower bounds and the min of the upper bounds.

{"require": {"php": "^8.1", "php-64bit": "^8.3"}}

gives int<80300, 80599> rather than int<80100, 80599>.

That change rides on the same toggle. It narrows the version range for projects declaring only php-64bit, which can flip a PHP_VERSION_ID >= X check to identical.alwaysTrue, so it carries the same upgrade risk as the int width.

Notes

  • ConfiguredPhpIntSizeHelper mirrors ConfiguredPhpVersionRangeHelper: NEON parameter first, composer.json second, null last.
  • phpIntSize is not in parametersNotInvalidatingCache, so the result cache invalidates on its own.
  • Adding #[AutowiredService] to a new class means composer dump-autoload before running anything locally.

Tests

  • PhpIntSize8Test runs data/php-int-size-8.php with phpIntSize: 8, gated on PHP_INT_SIZE === 8 because the expected values depend on the host's integer width. It asserts PHP_INT_MAX + 1 is a float.
  • ConfiguredPhpIntSizeHelperTest and ComposerPhpVersionFactoryTest cover the composer.json fixtures with the toggle on and off.

@VincentLanglet

Copy link
Copy Markdown
Contributor

I feel like that if such option was introduce it should support the 4 value.
I dunno how hard it would be to update the existing code to make it work.

@zonuexe

zonuexe commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

That's a fair thing to expect from the name, and it was my first instinct too. What stopped me is that 4 would promise something PHPStan cannot deliver.

phpIntSize only controls three constants. Everything downstream keeps the width of the PHP process running PHPStan. With phpIntSize: 8 set today:

\PHPStan\dumpType(3000000000);     // 3000000000
\PHPStan\dumpType(2147483647 + 1); // 2147483648
\PHPStan\dumpType(1 << 40);        // 1099511627776

Setting 4 would not change one of those. PHP_INT_MAX would say 2147483647 while 2147483647 + 1 still says int(2147483648), so the analysis would contradict itself and the user would have configured that contradiction on purpose. The same contradiction exists today, which is what phpstan/phpstan#14948 is about, but at least nobody asked for it.

Making 4 real means:

  • 27 places across 6 files read the host's PHP_INT_MAX / PHP_INT_MIN (IntegerRangeType 14, MutatingScope 8, TypeCombinator, UnionTypeHelper, TypeUtils, IntdivThrowTypeExtension)
  • literals: php-parser takes its tokens from PHP's own tokenizer, so 3000000000 arrives as an Int_ node on a 64bit host and a Float_ node on a 32bit one
  • every arithmetic operator in InitializerExprTypeResolver needs a 32bit overflow check
  • ConstantIntegerType can hold values a 32bit target cannot represent

That is option (1) in phpstan/phpstan#14948, which is what #11711 declined.

This PR doesn't pick a platform. The default stays 2147483647|9223372036854775807. The option only stops that union from deriving types no PHP build produces, and PHP_INT_MAX + 1 is the cheapest example.

If someone does need 32bit analysis, running PHPStan on a 32bit build already gets most of the way there, since the engine inherits its integer width from the process. 32bit is a thin release target in 2026, and the person writing PHP on a Raspberry Pi is coding and debugging on a 64bit machine.

If the name is what invites 4, I'm happy to rename it to something one-directional, assume64BitInt: true or similar. The behaviour is what I'd like to keep.

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 participants