# Comparison: Intersections

## Positive contributions

If we have an intersection type `A & B` and we get a definitive true/false answer for one of the
types, we can infer that the result for the intersection type is also true/false:

```py
from typing import Literal

class Base:
    def __gt__(self, other) -> bool:
        return False

class Child1(Base):
    def __eq__(self, other) -> Literal[True]:
        return True

class Child2(Base): ...

def _(x: Base):
    c1 = Child1()

    # Create an intersection type through narrowing:
    if isinstance(x, Child1):
        if isinstance(x, Child2):
            reveal_type(x)  # revealed: Child1 & Child2

            reveal_type(x == 1)  # revealed: Literal[True]

            # Other comparison operators fall back to the base type:
            reveal_type(x > 1)  # revealed: bool
            reveal_type(x is c1)  # revealed: bool
```

## Negative contributions

Negative contributions to the intersection type only allow simplifications in a few special cases
(equality and identity comparisons).

### Equality comparisons

#### Literal strings

```py
x = "x" * 1_000_000_000
y = "y" * 1_000_000_000
reveal_type(x)  # revealed: LiteralString

if x != "abc":
    reveal_type(x)  # revealed: LiteralString & ~Literal["abc"]

    # TODO: This should be `Literal[False]`
    reveal_type(x == "abc")  # revealed: bool
    # TODO: This should be `Literal[False]`
    reveal_type("abc" == x)  # revealed: bool
    reveal_type(x == "something else")  # revealed: bool
    reveal_type("something else" == x)  # revealed: bool

    # TODO: This should be `Literal[True]`
    reveal_type(x != "abc")  # revealed: bool
    # TODO: This should be `Literal[True]`
    reveal_type("abc" != x)  # revealed: bool
    reveal_type(x != "something else")  # revealed: bool
    reveal_type("something else" != x)  # revealed: bool

    reveal_type(x == y)  # revealed: bool
    reveal_type(y == x)  # revealed: bool
    reveal_type(x != y)  # revealed: bool
    reveal_type(y != x)  # revealed: bool

    reveal_type(x >= "abc")  # revealed: bool
    reveal_type("abc" >= x)  # revealed: bool

    reveal_type(x in "abc")  # revealed: bool
    reveal_type("abc" in x)  # revealed: bool
```

#### Integers

```py
def _(x: int):
    if x != 1:
        reveal_type(x)  # revealed: int & ~Literal[1] & ~Literal[True]

        reveal_type(x != 1)  # revealed: bool
        reveal_type(x != 2)  # revealed: bool

        reveal_type(x == 1)  # revealed: bool
        reveal_type(x == 2)  # revealed: bool
```

### Identity comparisons

The type `~None` excludes the `None` object, so its identity comparisons with `None` have definite
results.

```py
def _(o: object):
    n = None

    if o is not None:
        reveal_type(o)  # revealed: ~None
        reveal_type(o is n)  # revealed: Literal[False]
        reveal_type(o is not n)  # revealed: Literal[True]
```

A single-member enum contains only one object. A value excluded from `E` cannot be `E.ONLY`, so the
branch below is unreachable and must not emit an attribute error.

```py
from enum import Enum
from ty_extensions import Not

class E(Enum):
    ONLY = 1

def f(value: Not[E]) -> None:
    if value is E.ONLY:
        reveal_type(value)  # revealed: Never
        value.does_not_exist  # no error (unreachable branch)
```

A `NewType` negation removes its static tag, not the runtime objects of its base: an integer without
that tag can still be identical to the integer passed into the `NewType` constructor.

```py
from typing import NewType

UserId = NewType("UserId", int)

def f(value: Not[UserId]) -> None:
    reveal_type(value is 1)  # revealed: bool
```

After `not isinstance(value, B)`, `value` cannot be identical to a `B` instance. This remains true
when `value` has also been narrowed to `A`, so the inner branch is unreachable.

```py
class A: ...
class B: ...

def f(value: object, other_b: B) -> None:
    if isinstance(value, A) and not isinstance(value, B):
        if value is other_b:
            reveal_type(value)  # revealed: Never
            value.does_not_exist  # no error (unreachable branch)
```

## Diagnostics

### Unsupported operators for positive contributions

Raise an error if the given operator is unsupported for all positive contributions to the
intersection type:

```py
class NonContainer1: ...
class NonContainer2: ...

def _(x: object):
    if isinstance(x, NonContainer1):
        if isinstance(x, NonContainer2):
            reveal_type(x)  # revealed: NonContainer1 & NonContainer2

            # snapshot: unsupported-operator
            reveal_type(2 in x)  # revealed: bool
```

```snapshot
error[unsupported-operator]: Unsupported `in` operation
  --> src/mdtest_snippet.py:10:25
   |
10 |             reveal_type(2 in x)  # revealed: bool
   |                         -^^^^-
   |                         |    |
   |                         |    Has type `NonContainer1 & NonContainer2`
   |                         Has type `Literal[2]`
```

Do not raise an error if at least one of the positive contributions to the intersection type support
the operator:

```py
class Container:
    def __contains__(self, x) -> bool:
        return False

def _(x: object):
    if isinstance(x, NonContainer1):
        if isinstance(x, Container):
            if isinstance(x, NonContainer2):
                reveal_type(x)  # revealed: NonContainer1 & Container & NonContainer2
                reveal_type(2 in x)  # revealed: bool
```

Do also raise an error if the intersection has no positive contributions at all, unless the operator
is supported on `object`:

```py
def _(x: object):
    if not isinstance(x, NonContainer1):
        reveal_type(x)  # revealed: ~NonContainer1

        # snapshot: unsupported-operator
        reveal_type(2 in x)  # revealed: bool

        reveal_type(2 is x)  # revealed: bool
```

```snapshot
error[unsupported-operator]: Unsupported `in` operation
  --> src/mdtest_snippet.py:26:21
   |
26 |         reveal_type(2 in x)  # revealed: bool
   |                     -^^^^-
   |                     |    |
   |                     |    Has type `~NonContainer1`
   |                     Has type `Literal[2]`
```

### Unsupported operators for negative contributions

Do *not* raise an error if any of the negative contributions to the intersection type are
unsupported for the given operator:

```py
class Container:
    def __contains__(self, x) -> bool:
        return False

class NonContainer: ...

def _(x: object):
    if isinstance(x, Container):
        if not isinstance(x, NonContainer):
            reveal_type(x)  # revealed: Container & ~NonContainer

            # No error here!
            reveal_type(2 in x)  # revealed: bool
```
