Skip to content

non-negative-int evaluated to int #12163

Closed

Description

@sivlev

Bug report

To reproduce the unexpected behavior, I made a simplified snippet that contains essentially a method that iterates over two indices in a single for loop. Originally it comes from a piece of code dealing with 2D arrays (a matrix/linear algebra library). This snippet triggers Parameter #1 of method expects int<0, max>, int given, although I added a set of checks to make sure that both indices are non-negative during the iteration.

Interestingly, the error disappeared if:
either we replace $columnIndex = 0; with e.g. $columnIndex = 1; (a value that is > 0) on Line 20 before the loop
or we replace $columnIndex = 0; with e.g. $columnIndex = 1; (a value that is > 0) on Line 27 inside the loop

Code snippet that reproduces the problem

https://phpstan.org/r/3471ec19-7077-4e36-a3d4-27d2dc643fa8

Expected output

It is expected that the parameter/variable remains non-negative-int.

Did PHPStan help you today? Did it make you happy in any way?

Excellent tools for checking/dealing with non-empty-list and non-negative-int in my linear algebra objects :)

Activity

  1. added this to the Easy fixes milestone on Dec 1, 2024
  2. VincentLanglet commented on Mar 30, 2025

    @VincentLanglet
    Contributor

    I feel like it's an easy fix I could try ; any idea where I should look in the code @ondrejmirtes ?

  3. herndlm commented on Mar 30, 2025

    @herndlm
    Contributor

    There's a loop involved. That always changes my "simple" to "could be impossible" 😁
    Having said that, I would step-debug through the loop analysis to figure out more.

  4. ondrejmirtes commented on Mar 30, 2025

    @ondrejmirtes
    Member

    Yeah, the logic is wrong probably somewhere here: https://github.com/phpstan/phpstan-src/blob/b225f74805542bddc5249156d84143c0eddbc51e/src/Analyser/MutatingScope.php#L5130-L5440

    This code (called from https://github.com/phpstan/phpstan-src/blob/b225f74805542bddc5249156d84143c0eddbc51e/src/Analyser/NodeScopeResolver.php#L1432-L1438 in case of for loops) deals with differentiating whether a loop does $i++ (an ever-changing value which has to be generalized), or $i = 3 (stable value that can be kept as 3).

  5. VincentLanglet commented on Mar 30, 2025

    @VincentLanglet
    Contributor

    After some tries, it seems that MutatingScope::generalizeType has the following behavior:

    • generalizeType(0, 0|1) => int<0, max>
    • generalizeType(1, 0|1) => int<min, 1>
    • generalizeType(1, 1|2) => int<min, 1>
    • generalizeType(1, 0|1|2) => int<min, max>
    • generalizeType(0, int<0, 1>) => int<0, 1>
    • generalizeType(1, int<0, 1>) => int<0, 1>
    • generalizeType(1, int<1, 2>) => int<1, 2>
    • generalizeType(1, int<0, 2>) => int<0, 2>

    So a union of integers and an integerRange does not give the same results.

    I'm unsure if TypeCombinator::union should always transform union of scalar into integerRange like

            $constantIntegerTypes = $scalarTypes[ConstantIntegerType::class] ?? [];
    		$count = count($constantIntegerTypes);
    		if ($count > 1) {
    			$constantIntegerValues = array_map(
    				static fn (ConstantIntegerType $type): int => $type->getValue(),
    				$constantIntegerTypes,
    			);
    			sort($constantIntegerValues);
    			if ($constantIntegerValues[$count - 1] - $constantIntegerValues[0] === $count - 1) {
    				$integerRangeTypes[] = IntegerRangeType::fromInterval($constantIntegerValues[0], $constantIntegerValues[$count - 1]);
    				unset($scalarTypes[ConstantIntegerType::class]);
    			}
    		}
    

    or if this logic should be only done inside MutatingScope::generalizeType or if it's the wrong direction...

  6. staabm commented on Mar 30, 2025

    @staabm
    Contributor

    My gut feeling is that

    generalizeType(1, 0|1|2) => int<min, max>

    Should lead to int<0, 2> instead. Whether this fixes the problem is another question :-)

  7. VincentLanglet commented on Mar 30, 2025

    @VincentLanglet
    Contributor

    My gut feeling is that

    generalizeType(1, 0|1|2) => int<min, max>

    Should lead to int<0, 2> instead. Whether this fixes the problem is another question :-)

    I now understand why it's that simplified.
    phpstan/phpstan-src@b225f74/src/Analyser/NodeScopeResolver.php#L1432-L1438 calls generalizeType only a limiting time. And if we don't generalize generalizeType(1, 0|1|2) we end up with 100 failures because lot of int<0, max> integers are now considered 0|1|2|3|4|5|6 or similar unions.

    There's a loop involved. That always changes my "simple" to "could be impossible"

    I now understand ^^'

  8. added a commit that references this issue on Mar 12, 2026
    9d660fb
  9. phpstan-bot commented on Mar 12, 2026

    @phpstan-bot
    Contributor

    Closed via merging phpstan/phpstan-src#5176

  10. added 2 commits that reference this issue on Mar 12, 2026
    9d660fb
    e8d1dee
  11. added a commit that references this issue on Apr 7, 2026
    0f55eca
  12. github-actions commented on Apr 13, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

  13. locked as resolved and limited conversation to collaborators on Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions