Skip to content

Fix bool narrowing for numeric literal patterns - #11570

Merged
rchiodo merged 1 commit into
microsoft:mainfrom
hsusul:fix/match-numeric-literal-bool-narrowing
Jul 28, 2026
Merged

Fix bool narrowing for numeric literal patterns#11570
rchiodo merged 1 commit into
microsoft:mainfrom
hsusul:fix/match-numeric-literal-bool-narrowing

Conversation

@hsusul

@hsusul hsusul commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Preserve Literal[False] and Literal[True] when matching the numeric literal patterns 0 and 1.
  • Remove those bool literals from the fallthrough branch because numeric literal patterns compare with equality.
  • Keep singleton bool patterns identity-based, so case False does not match Literal[0].

Problem

Numeric literal patterns use equality at runtime. This means case 0 matches both 0 and False, and case 1 matches both 1 and True.

from typing import Literal

def f(value: Literal[0, False]) -> None:
    match value:
        case 0:
            reveal_type(value)  # Literal[0, False]
        case _:
            reveal_type(value)  # Never

Pyright previously narrowed the matched branch to Literal[0] and the fallthrough branch to Literal[False], which is unsound relative to runtime behavior.

Root cause

Literal-pattern narrowing relied on assignability between the pattern literal and subject literal. That does not capture Python's equality relationship between 0/False and 1/True.

Solution

Handle the asymmetric numeric-pattern equality case explicitly:

  • an int literal pattern 0 or 1 retains the equal bool literal in the positive branch;
  • the same bool literal is removed from the negative branch;
  • bool singleton patterns remain unchanged because they use identity rather than equality.

Tests

Added focused coverage for:

  • case 0 over Literal[0, False];
  • case 1 over Literal[1, True];
  • case False retaining the identity-based distinction from Literal[0].

Validation

  • npm run typecheck
  • npm run build:cli:dev
  • npm run check
  • npm run build in packages/pyright-internal
  • npx jest -t 'MatchLiteral1' --forceExit --runInBand
  • npx jest src/tests/typeEvaluator6.test.ts --runInBand
  • npm test -- --runInBand (62 suites, 2549 tests)
  • CLI repro on Python 3.10 and 3.13

Compatibility and risk

The change is limited to literal-pattern narrowing for int patterns against bool literal subjects. Tests cover both positive and negative branches and the inverse singleton-bool boundary.

Documentation

No documentation changes are needed; this aligns analyzer behavior with Python's existing structural pattern-matching semantics.

Related issues and prior work

#11026 and #11028 addressed broader literal-pattern narrowing involving non-literal and supertype subjects. This change covers the distinct bool/int literal equality edge case.

@rchiodo

rchiodo commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

🔒 Automated review in progress — @rchiodo is auto-reviewing this PR.

@rchiodo rchiodo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved via Review Center.

@rchiodo rchiodo added the review-auto:approved Automated review: no blocking findings (approval posted). label Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

sympy (https://github.com/sympy/sympy)
+   .../projects/sympy/sympy/solvers/ode/subscheck.py:150:48 - error: Argument of type "int | Unknown | str" cannot be assigned to parameter "order" of type "str" in function "checkodesol"
+     Type "int | Unknown | str" is not assignable to type "str"
+       "int" is not assignable to "str" (reportArgumentType)
-   .../projects/sympy/sympy/solvers/ode/systems.py:1167:37 - error: Operator "*" not supported for types "Literal[2]" and "Unknown | Self@Basic | bool | Expr | Basic"
+   .../projects/sympy/sympy/solvers/ode/systems.py:1167:37 - error: Operator "*" not supported for types "Literal[2]" and "Unknown | Expr | Basic | bool"
-     Operator "*" not supported for types "Literal[2]" and "Basic*"
-   .../projects/sympy/sympy/solvers/ode/systems.py:1167:37 - error: Operator "*" not supported for types "Unknown | int | Expr" and "Unknown | Self@Basic | bool | Expr | Basic"
+   .../projects/sympy/sympy/solvers/ode/systems.py:1167:37 - error: Operator "*" not supported for types "Unknown | Expr | int" and "Unknown | Expr | Basic | bool"
-     Operator "*" not supported for types "int" and "Basic*"
-     Operator "*" not supported for types "int" and "Basic"
-     Operator "*" not supported for types "Expr" and "Basic*"
+     Operator "*" not supported for types "Expr" and "Basic"
+     Operator "*" not supported for types "int" and "Basic" (reportOperatorIssue)
-   .../projects/sympy/sympy/solvers/ode/systems.py:1169:30 - error: Operator "*" not supported for types "Literal[2]" and "Unknown | Self@Basic | bool | Expr | Basic"
+   .../projects/sympy/sympy/solvers/ode/systems.py:1169:30 - error: Operator "*" not supported for types "Literal[2]" and "Unknown | Expr | Basic | bool"
-     Operator "*" not supported for types "Literal[2]" and "Basic*"
-   .../projects/sympy/sympy/solvers/ode/systems.py:1169:30 - error: Operator "*" not supported for types "Unknown | int | Expr" and "Unknown | Self@Basic | bool | Expr | Basic"
+   .../projects/sympy/sympy/solvers/ode/systems.py:1169:30 - error: Operator "*" not supported for types "Unknown | Expr | int" and "Unknown | Expr | Basic | bool"
-     Operator "*" not supported for types "int" and "Basic*"
-     Operator "*" not supported for types "int" and "Basic"
-     Operator "*" not supported for types "Expr" and "Basic*"
+     Operator "*" not supported for types "Expr" and "Basic"
+     Operator "*" not supported for types "int" and "Basic" (reportOperatorIssue)
-   .../projects/sympy/sympy/solvers/ode/systems.py:1169:49 - error: Operator "*" not supported for types "Literal[2]" and "Unknown | Self@Basic | bool | Expr | Basic"
+   .../projects/sympy/sympy/solvers/ode/systems.py:1169:49 - error: Operator "*" not supported for types "Literal[2]" and "Unknown | Expr | Basic | bool"
-     Operator "*" not supported for types "Literal[2]" and "Basic*"
-   .../projects/sympy/sympy/solvers/ode/systems.py:1169:49 - error: Operator "*" not supported for types "Unknown | int | Expr" and "Unknown | Self@Basic | bool | Expr | Basic"
+   .../projects/sympy/sympy/solvers/ode/systems.py:1169:49 - error: Operator "*" not supported for types "Unknown | Expr | int" and "Unknown | Expr | Basic | bool"
-     Operator "*" not supported for types "int" and "Basic*"
-     Operator "*" not supported for types "int" and "Basic"
-     Operator "*" not supported for types "Expr" and "Basic*"
+     Operator "*" not supported for types "Expr" and "Basic"
+     Operator "*" not supported for types "int" and "Basic" (reportOperatorIssue)
+   .../projects/sympy/sympy/stats/crv.py:369:24 - error: Operator "*" not supported for types "Expr" and "Unknown | Basic"
+   .../projects/sympy/sympy/stats/crv.py:379:25 - error: Operator "*" not supported for types "Expr" and "Unknown | Basic"
+   .../projects/sympy/sympy/stats/crv.py:392:29 - error: Operator "-" not supported for types "Unknown | Basic" and "Dummy"
+     Operator "-" not supported for types "Basic" and "Dummy" (reportOperatorIssue)
+   .../projects/sympy/sympy/stats/crv_types.py:1289:16 - error: Operator "-" not supported for types "Unknown | Basic | Any" and "Expr"
+     Operator "-" not supported for types "Basic" and "Expr" (reportOperatorIssue)
+   .../projects/sympy/sympy/stats/crv_types.py:3358:23 - error: Operator "*" not supported for types "Expr" and "Unknown | Basic | Any"
+   .../projects/sympy/sympy/stats/drv.py:266:18 - error: No overloads for "sum" match the provided arguments (reportCallIssue)
+   .../projects/sympy/sympy/stats/drv.py:266:22 - error: Argument of type "Generator[Unknown | Basic, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
+     "Generator[Unknown | Basic, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
+       Type parameter "_T_co@Iterable" is covariant, but "Unknown | Basic" is not a subtype of "_SupportsSumNoDefaultT@sum"
+         Type "Unknown | Basic" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+           Type "Unknown | Basic" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+             "Basic" is incompatible with protocol "_SupportsSumWithNoDefaultGiven" (reportArgumentType)
-   .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
+   .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
-     "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
+     "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
-       Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
+       Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
-         Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+         Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
-           Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+           Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
-   .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic"
+   .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic"
-   .../projects/sympy/sympy/stats/frv.py:172:24 - error: Cannot access attribute "lhs" for class "Basic*"
+   .../projects/sympy/sympy/stats/frv.py:172:24 - error: Cannot access attribute "lhs" for class "Basic"
-   .../projects/sympy/sympy/stats/frv.py:172:35 - error: Cannot access attribute "rhs" for class "Basic*"
+   .../projects/sympy/sympy/stats/frv.py:172:35 - error: Cannot access attribute "rhs" for class "Basic"
-     Return type mismatch: base method returns type "bool", override returns type "Unknown | Basic* | Literal[False]"
+     Return type mismatch: base method returns type "bool", override returns type "Unknown | Basic | Literal[False]"
-       Type "Unknown | Basic* | Literal[False]" is not assignable to type "bool"
+       Type "Unknown | Basic | Literal[False]" is not assignable to type "bool"
-         "Basic*" is not assignable to "bool" (reportIncompatibleMethodOverride)
+         "Basic" is not assignable to "bool" (reportIncompatibleMethodOverride)
+   .../projects/sympy/sympy/stats/frv.py:401:34 - error: Operator "*" not supported for types "Unknown | Basic | Literal[0]" and "Expr"
+     Operator "*" not supported for types "Basic" and "Expr" (reportOperatorIssue)
+   .../projects/sympy/sympy/stats/frv.py:411:34 - error: Operator "*" not supported for types "Unknown | Basic | Literal[0]" and "Expr"
+     Operator "*" not supported for types "Basic" and "Expr" (reportOperatorIssue)
-   .../projects/sympy/sympy/stats/joint_rv_types.py:576:27 - error: Argument of type "One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | Unknown | Number | Expr" cannot be assigned to parameter "stop" of type "SupportsIndex" in function "__new__"
+   .../projects/sympy/sympy/stats/joint_rv_types.py:576:27 - error: Argument of type "One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | Unknown | Infinity | NegativeInfinity | Float | Number | Expr" cannot be assigned to parameter "stop" of type "SupportsIndex" in function "__new__"
-     Type "One | NegativeOne | Zero | Integer | NaN | ComplexInfinity | Rational | Unknown | Number | Expr" is not assignable to type "SupportsIndex"

... (truncated 527 lines) ...

@rchiodo

rchiodo commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

The sympy mypy_primer diffs are not caused by this PR — they're pre-existing pyright non-determinism

I built pyright at this PR's base (729aabfa3, == current main) and at the PR head (37d3fc744) and ran both on the full sympy source in an identical environment.

The two builds produce identical diagnostics on sympy:

  • Base vs PR: 0 added, 0 removed (75,789 diagnostics each), comparing full multi-line messages.
  • Determinism check (same binary run twice): also 0 diff.

So on a deterministic machine the change has zero effect on sympy — a real, PR-caused diff would have reproduced.

Why that's expected from the code: the new isIntLiteralPatternEqualToBool path only activates when a numeric literal pattern 0/1 is matched against a bool-literal subject (Literal[True]/Literal[False]) in a match/case. In all of sympy:

  • The only case 0:/case 1: is trigonometric.py's match n % 4:, whose subject is int (not a bool literal) → the new path never triggers.
  • None of the flagged files (subscheck.py, systems.py, crv.py, crv_types.py, drv.py, drv_types.py, frv.py, joint_rv_types.py) contain any match/case.

What the primer diff actually is: analysis-order / recursion-resolution non-determinism over sympy's recursive Basic/Expr/Self hierarchy. The signatures are the usual tells — none of which a literal-pattern-narrowing change can produce:

  • Basic*Basic (the * marks a TypeVar/Self-solved type)
  • Self@Basic dropping in/out of unions
  • union member reordering (e.g. Unknown | Self@Basic | bool | Expr | BasicUnknown | Expr | Basic | bool)
  • Add / Infinity | NegativeInfinity | Float appearing/disappearing in large unions

Bottom line: expected primer noise, not a new bug.

@rchiodo
rchiodo merged commit 559a737 into microsoft:main Jul 28, 2026
16 checks passed
@rchiodo

rchiodo commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants