[red-knot] Disjointness for callable types (#17094)

## Summary

Part of #15382, this PR adds support for disjointness between two
callable types. They are never disjoint because there exists a callable
type that's a subtype of all other callable types:
```py
(*args: object, **kwargs: object) -> Never
```

The `Never` is a subtype of every fully static type thus a callable type
that has the return type of `Never` means that it is a subtype of every
return type.

## Test Plan

Add test cases related to mixed parameter kinds, gradual form (`...`)
and `Never` type.
This commit is contained in:
Dhruv Manilawala 2025-04-02 00:30:27 +05:30 committed by GitHub
parent d6dcc377f7
commit 6be0a5057d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 6 deletions

View file

@ -0,0 +1,15 @@
# Tuple
## `Never`
If a tuple type contains a `Never` element, then it is eagerly simplified to `Never` which means
that a tuple type containing `Never` is disjoint from any other tuple type.
```py
from typing_extensions import Never
def _(x: tuple[Never], y: tuple[int, Never], z: tuple[Never, int]):
reveal_type(x) # revealed: Never
reveal_type(y) # revealed: Never
reveal_type(z) # revealed: Never
```

View file

@ -61,7 +61,7 @@ static_assert(is_disjoint_from(B2, FinalSubclass))
## Tuple types
```py
from typing_extensions import Literal
from typing_extensions import Literal, Never
from knot_extensions import TypeOf, is_disjoint_from, static_assert
static_assert(is_disjoint_from(tuple[()], TypeOf[object]))
@ -353,3 +353,29 @@ class UsesMeta2(metaclass=Meta2): ...
static_assert(is_disjoint_from(type[UsesMeta1], type[UsesMeta2]))
```
## Callables
No two callable types are disjoint because there exists a non-empty callable type
`(*args: object, **kwargs: object) -> Never` that is a subtype of all fully static callable types.
As such, for any two callable types, it is possible to conceive of a runtime callable object that
would inhabit both types simultaneously.
```py
from knot_extensions import CallableTypeOf, is_disjoint_from, static_assert
from typing_extensions import Callable, Literal, Never
def mixed(a: int, /, b: str, *args: int, c: int = 2, **kwargs: int) -> None: ...
static_assert(not is_disjoint_from(Callable[[], Never], CallableTypeOf[mixed]))
static_assert(not is_disjoint_from(Callable[[int, str], float], CallableTypeOf[mixed]))
# Using gradual form
static_assert(not is_disjoint_from(Callable[..., None], Callable[[], None]))
static_assert(not is_disjoint_from(Callable[..., None], Callable[..., None]))
static_assert(not is_disjoint_from(Callable[..., None], Callable[[Literal[1]], None]))
# Using `Never`
static_assert(not is_disjoint_from(Callable[[], Never], Callable[[], Never]))
static_assert(not is_disjoint_from(Callable[[Never], str], Callable[[Never], int]))
```

View file

@ -1343,6 +1343,19 @@ impl<'db> Type<'db> {
.to_instance(db)
.is_disjoint_from(db, other),
(Type::Callable(_) | Type::FunctionLiteral(_), Type::Callable(_))
| (Type::Callable(_), Type::FunctionLiteral(_)) => {
// No two callable types are ever disjoint because
// `(*args: object, **kwargs: object) -> Never` is a subtype of all fully static
// callable types.
false
}
(Type::Callable(_), _) | (_, Type::Callable(_)) => {
// TODO: Implement disjointness for general callable type with other types
false
}
(Type::ModuleLiteral(..), other @ Type::Instance(..))
| (other @ Type::Instance(..), Type::ModuleLiteral(..)) => {
// Modules *can* actually be instances of `ModuleType` subclasses
@ -1379,11 +1392,6 @@ impl<'db> Type<'db> {
// TODO: add checks for the above cases once we support them
instance.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
}
(Type::Callable(_), _) | (_, Type::Callable(_)) => {
// TODO: Implement disjointedness for callable types
false
}
}
}