Skip to content

Narrow division to int when modulo is known to be zero #5757

Open
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-uzkbeoe
Open

Narrow division to int when modulo is known to be zero #5757
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-uzkbeoe

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented May 25, 2026

…s in `integerRangeMath`

- Return benevolent union from `integerRangeMath` when division of two integer
  types produces `int<range>|float`, matching the existing behavior for plain
  `int / int` division
- Propagate benevolence through union-combining code in both `integerRangeMath`
  (operand union handling) and `resolveCommonMath` (left number type union
  handling) by tracking whether any part returned a `BenevolentUnionType`
- Simplify the cross-zero range-splitting blocks to use
  `TypeUtils::toBenevolentUnion()` instead of an equality check against
  `UnionType([int, float])`, which missed bounded integer range results
- Update test expectations in integer-range-types.php, div-by-zero.php,
  math.php, and binary.php to reflect the corrected benevolent union types
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be instead possible to say that

0 === ($offset % $limit) imply $offset / $limit is an int ?

Instead of making all integer range divisions return benevolent unions,
detect when the scope knows that `$a % $b === 0` and narrow `$a / $b`
to just the integer type (removing float from the union).

This is more precise: it only removes float when mathematically
guaranteed, rather than broadly making all integer range divisions
benevolent. The check works by constructing a synthetic modulo
expression with the same operands and querying the scope for its type.
When a condition like `0 === ($a % $b)` has narrowed the modulo to
ConstantIntegerType(0), the division result is guaranteed to be int.

Reverts the BenevolentUnionType propagation changes from the previous
commit since the modulo narrowing is the correct fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's a summary of what changed:

Approach change: Instead of making all integer range divisions return benevolent unions (the previous commit's approach), the fix now detects when $a % $b is known to be 0 in the current scope and narrows $a / $b to just int by removing float from the result type.

How it works: In getDivType(), after computing the normal division result, a synthetic BinaryOp\Mod expression is constructed with the same operands and queried through the scope's type resolver. When a condition like 0 === ($a % $b) has narrowed the modulo type to ConstantIntegerType(0), TypeCombinator::remove() strips float from the division result.

Changes:

  • InitializerExprTypeResolver::getDivType() — added modulo-based narrowing
  • Reverted all BenevolentUnionType propagation changes from the previous commit (resolveCommonMath, integerRangeMath)
  • bug-9724.php test — assertions now expect precise int types instead of benevolent (float|int) unions
  • Reverted test expectation changes in div-by-zero.php, integer-range-types.php, math.php

@VincentLanglet VincentLanglet changed the title Preserve BenevolentUnionType for IntegerRangeType division results in integerRangeMath Narrow division to int when modulo is known to be zero May 25, 2026
return $this->getDivTypeFromTypes($left, $right, $leftType, $rightType);
$result = $this->getDivTypeFromTypes($left, $right, $leftType, $rightType);

if ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this logic be in getDivTypeFromTypes instead ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed. The change moves the modulo-based narrowing logic from getDivType into getDivTypeFromTypes, addressing VincentLanglet's review comment. getDivType now stays a thin wrapper (consistent with getMulType, getModType, etc.) — it resolves the mod type via the callback and passes it to getDivTypeFromTypes as an optional ?Type $modType = null parameter. The actual narrowing (TypeCombinator::remove($result, new FloatType())) happens inside getDivTypeFromTypes.

All tests pass (12154 tests, 79829 assertions) and make phpstan reports no errors.

@VincentLanglet VincentLanglet self-assigned this May 25, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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