[red-knot] Check gradual equivalence between callable types (#16634)

This commit is contained in:
Dhruv Manilawala 2025-03-13 08:16:51 +05:30 committed by GitHub
parent 08fa9b4a90
commit 58d5fe982e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 110 additions and 0 deletions

View file

@ -62,4 +62,67 @@ static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, byte
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
```
## Callable
```py
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
from typing import Any, Callable
static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int]))
static_assert(is_gradual_equivalent_to(Callable[..., Any], Callable[..., Unknown]))
static_assert(is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None]))
static_assert(not is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None]))
static_assert(not is_gradual_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None]))
static_assert(not is_gradual_equivalent_to(Callable[..., None], Callable[[], None]))
```
A function with no explicit return type should be gradual equivalent to a callable with a return
type of `Any`.
```py
def f1():
return
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[], Any]))
```
And, similarly for parameters with no annotations.
```py
def f2(a, b) -> None:
return
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
```
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
with `...` as the parameter type.
```py
def variadic_without_annotation(*args, **kwargs):
return
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
return
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without_annotation], Callable[..., Any]))
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
```
But, a function with either `*args` or `**kwargs` is not gradual equivalent to a callable with `...`
as the parameter type.
```py
def variadic_args(*args):
return
def variadic_kwargs(**kwargs):
return
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_args], Callable[..., Any]))
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
```
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize

View file

@ -956,6 +956,11 @@ impl<'db> Type<'db> {
first.is_gradual_equivalent_to(db, second)
}
(
Type::Callable(CallableType::General(first)),
Type::Callable(CallableType::General(second)),
) => first.is_gradual_equivalent_to(db, second),
_ => false,
}
}
@ -4575,6 +4580,48 @@ impl<'db> GeneralCallableType<'db> {
.return_ty
.is_some_and(|return_type| return_type.is_fully_static(db))
}
/// Return `true` if `self` has exactly the same set of possible static materializations as
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
/// `other`).
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_signature = self.signature(db);
let other_signature = other.signature(db);
if self_signature.parameters().len() != other_signature.parameters().len() {
return false;
}
// Check gradual equivalence between the two optional types. In the context of a callable
// type, the `None` type represents an `Unknown` type.
let are_optional_types_gradually_equivalent =
|self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| {
self_type
.unwrap_or(Type::unknown())
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
};
if !are_optional_types_gradually_equivalent(
self_signature.return_ty,
other_signature.return_ty,
) {
return false;
}
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
// parameters because it is internally represented by adding `*Any` and `**Any` to the
// parameter list.
self_signature
.parameters()
.iter()
.zip(other_signature.parameters().iter())
.all(|(self_param, other_param)| {
are_optional_types_gradually_equivalent(
self_param.annotated_type(),
other_param.annotated_type(),
)
})
}
}
/// A type that represents callable objects.