Skip to content

Commit 58d5fe9

Browse files
authored
[red-knot] Check gradual equivalence between callable types (#16634)
1 parent 08fa9b4 commit 58d5fe9

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

crates/red_knot_python_semantic/resources/mdtest/type_properties/is_gradual_equivalent_to.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,67 @@ static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[str, int, byte
6262
static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
6363
```
6464

65+
## Callable
66+
67+
```py
68+
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
69+
from typing import Any, Callable
70+
71+
static_assert(is_gradual_equivalent_to(Callable[..., int], Callable[..., int]))
72+
static_assert(is_gradual_equivalent_to(Callable[..., Any], Callable[..., Unknown]))
73+
static_assert(is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[int, Unknown], None]))
74+
75+
static_assert(not is_gradual_equivalent_to(Callable[[int, Any], None], Callable[[Any, int], None]))
76+
static_assert(not is_gradual_equivalent_to(Callable[[int, str], None], Callable[[int, str, bytes], None]))
77+
static_assert(not is_gradual_equivalent_to(Callable[..., None], Callable[[], None]))
78+
```
79+
80+
A function with no explicit return type should be gradual equivalent to a callable with a return
81+
type of `Any`.
82+
83+
```py
84+
def f1():
85+
return
86+
87+
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[], Any]))
88+
```
89+
90+
And, similarly for parameters with no annotations.
91+
92+
```py
93+
def f2(a, b) -> None:
94+
return
95+
96+
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
97+
```
98+
99+
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
100+
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
101+
with `...` as the parameter type.
102+
103+
```py
104+
def variadic_without_annotation(*args, **kwargs):
105+
return
106+
107+
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
108+
return
109+
110+
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without_annotation], Callable[..., Any]))
111+
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
112+
```
113+
114+
But, a function with either `*args` or `**kwargs` is not gradual equivalent to a callable with `...`
115+
as the parameter type.
116+
117+
```py
118+
def variadic_args(*args):
119+
return
120+
121+
def variadic_kwargs(**kwargs):
122+
return
123+
124+
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_args], Callable[..., Any]))
125+
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
126+
```
127+
65128
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize

crates/red_knot_python_semantic/src/types.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,11 @@ impl<'db> Type<'db> {
956956
first.is_gradual_equivalent_to(db, second)
957957
}
958958

959+
(
960+
Type::Callable(CallableType::General(first)),
961+
Type::Callable(CallableType::General(second)),
962+
) => first.is_gradual_equivalent_to(db, second),
963+
959964
_ => false,
960965
}
961966
}
@@ -4575,6 +4580,48 @@ impl<'db> GeneralCallableType<'db> {
45754580
.return_ty
45764581
.is_some_and(|return_type| return_type.is_fully_static(db))
45774582
}
4583+
4584+
/// Return `true` if `self` has exactly the same set of possible static materializations as
4585+
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
4586+
/// `other`).
4587+
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
4588+
let self_signature = self.signature(db);
4589+
let other_signature = other.signature(db);
4590+
4591+
if self_signature.parameters().len() != other_signature.parameters().len() {
4592+
return false;
4593+
}
4594+
4595+
// Check gradual equivalence between the two optional types. In the context of a callable
4596+
// type, the `None` type represents an `Unknown` type.
4597+
let are_optional_types_gradually_equivalent =
4598+
|self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| {
4599+
self_type
4600+
.unwrap_or(Type::unknown())
4601+
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
4602+
};
4603+
4604+
if !are_optional_types_gradually_equivalent(
4605+
self_signature.return_ty,
4606+
other_signature.return_ty,
4607+
) {
4608+
return false;
4609+
}
4610+
4611+
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
4612+
// parameters because it is internally represented by adding `*Any` and `**Any` to the
4613+
// parameter list.
4614+
self_signature
4615+
.parameters()
4616+
.iter()
4617+
.zip(other_signature.parameters().iter())
4618+
.all(|(self_param, other_param)| {
4619+
are_optional_types_gradually_equivalent(
4620+
self_param.annotated_type(),
4621+
other_param.annotated_type(),
4622+
)
4623+
})
4624+
}
45784625
}
45794626

45804627
/// A type that represents callable objects.

0 commit comments

Comments
 (0)