Skip to content

Commit 707c6fd

Browse files
committed
[red-knot] Check whether two callable types are equivalent
1 parent 81759be commit 707c6fd

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed

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

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,107 @@ class R: ...
118118
static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R]))
119119
```
120120

121+
## Callable
122+
123+
### Equivalent
124+
125+
For an equivalence relationship, the default value does not necessarily need to be the same but if
126+
the parameter in one of the callable has a default value then the corresponding parameter in the
127+
other callable should also have a default value.
128+
129+
```py
130+
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
131+
from typing import Callable
132+
133+
def c1(a: int, /, b: float, c: bool = False, *args: int, d: int = 1, e: str, **kwargs: float) -> None: ...
134+
def c2(a: int, /, b: float, c: bool = True, *args: int, d: int = 2, e: str, **kwargs: float) -> None: ...
135+
136+
static_assert(is_equivalent_to(CallableTypeFromFunction[c1], CallableTypeFromFunction[c2]))
137+
```
138+
139+
### Not equivalent
140+
141+
There are multiple cases when two callable types are not equivalent which are enumerated below.
142+
143+
```py
144+
from knot_extensions import CallableTypeFromFunction, is_equivalent_to, static_assert
145+
from typing import Callable
146+
```
147+
148+
When the number of parameters is different:
149+
150+
```py
151+
def f1(a: int) -> None: ...
152+
def f2(a: int, b: int) -> None: ...
153+
154+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
155+
```
156+
157+
When either of the callable types uses a gradual form for the parameters:
158+
159+
```py
160+
static_assert(not is_equivalent_to(Callable[..., None], Callable[[int], None]))
161+
static_assert(not is_equivalent_to(Callable[[int], None], Callable[..., None]))
162+
```
163+
164+
When the return types are not equivalent or absent in one or both of the callable types:
165+
166+
```py
167+
def f3(): ...
168+
def f4() -> None: ...
169+
170+
static_assert(not is_equivalent_to(Callable[[], int], Callable[[], None]))
171+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f3]))
172+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
173+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f4], CallableTypeFromFunction[f3]))
174+
```
175+
176+
When the parameter names are different:
177+
178+
```py
179+
def f5(a: int) -> None: ...
180+
def f6(b: int) -> None: ...
181+
182+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], CallableTypeFromFunction[f6]))
183+
```
184+
185+
When only one of the callable types has parameter names:
186+
187+
```py
188+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f5], Callable[[int], None]))
189+
```
190+
191+
When the parameter kinds are different:
192+
193+
```py
194+
def f7(a: int, /) -> None: ...
195+
def f8(a: int) -> None: ...
196+
197+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f7], CallableTypeFromFunction[f8]))
198+
```
199+
200+
When the annotated types of the parameters are not equivalent or absent in one or both of the
201+
callable types:
202+
203+
```py
204+
def f9(a: int) -> None: ...
205+
def f10(a: str) -> None: ...
206+
def f11(a) -> None: ...
207+
208+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f9], CallableTypeFromFunction[f10]))
209+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f10], CallableTypeFromFunction[f11]))
210+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f10]))
211+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f11], CallableTypeFromFunction[f11]))
212+
```
213+
214+
When the default value for a parameter is present only in one of the callable type:
215+
216+
```py
217+
def f12(a: int) -> None: ...
218+
def f13(a: int = 2) -> None: ...
219+
220+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f12], CallableTypeFromFunction[f13]))
221+
static_assert(not is_equivalent_to(CallableTypeFromFunction[f13], CallableTypeFromFunction[f12]))
222+
```
223+
121224
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent

crates/red_knot_python_semantic/src/types.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,10 @@ impl<'db> Type<'db> {
898898
left.is_equivalent_to(db, right)
899899
}
900900
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
901+
(
902+
Type::Callable(CallableType::General(left)),
903+
Type::Callable(CallableType::General(right)),
904+
) => left.is_equivalent_to(db, right),
901905
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
902906
}
903907
}
@@ -4542,6 +4546,77 @@ impl<'db> GeneralCallableType<'db> {
45424546
.is_some_and(|return_type| return_type.is_fully_static(db))
45434547
}
45444548

4549+
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`.
4550+
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
4551+
let self_signature = self.signature(db);
4552+
let other_signature = other.signature(db);
4553+
4554+
let self_parameters = self_signature.parameters();
4555+
let other_parameters = other_signature.parameters();
4556+
4557+
if self_parameters.len() != other_parameters.len() {
4558+
return false;
4559+
}
4560+
4561+
if self_parameters.is_gradual() || other_parameters.is_gradual() {
4562+
return false;
4563+
}
4564+
4565+
// Check equivalence relationship between two optional types. If either of them is `None`,
4566+
// then it is not a fully static type which means it's not equivalent either.
4567+
let is_equivalent = |self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| match (
4568+
self_type, other_type,
4569+
) {
4570+
(Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type),
4571+
_ => false,
4572+
};
4573+
4574+
if !is_equivalent(self_signature.return_ty, other_signature.return_ty) {
4575+
return false;
4576+
}
4577+
4578+
for (self_parameter, other_parameter) in self_parameters.iter().zip(other_parameters) {
4579+
if self_parameter.name() != other_parameter.name() {
4580+
return false;
4581+
}
4582+
4583+
if self_parameter.default_type().is_some() != other_parameter.default_type().is_some() {
4584+
return false;
4585+
}
4586+
4587+
if !matches!(
4588+
(self_parameter.kind(), other_parameter.kind()),
4589+
(
4590+
ParameterKind::PositionalOnly { .. },
4591+
ParameterKind::PositionalOnly { .. }
4592+
) | (
4593+
ParameterKind::PositionalOrKeyword { .. },
4594+
ParameterKind::PositionalOrKeyword { .. }
4595+
) | (
4596+
ParameterKind::Variadic { .. },
4597+
ParameterKind::Variadic { .. }
4598+
) | (
4599+
ParameterKind::KeywordOnly { .. },
4600+
ParameterKind::KeywordOnly { .. }
4601+
) | (
4602+
ParameterKind::KeywordVariadic { .. },
4603+
ParameterKind::KeywordVariadic { .. }
4604+
)
4605+
) {
4606+
return false;
4607+
}
4608+
4609+
if !is_equivalent(
4610+
self_parameter.annotated_type(),
4611+
other_parameter.annotated_type(),
4612+
) {
4613+
return false;
4614+
}
4615+
}
4616+
4617+
true
4618+
}
4619+
45454620
/// Return `true` if `self` has exactly the same set of possible static materializations as
45464621
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
45474622
/// `other`).

crates/red_knot_python_semantic/src/types/signatures.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,11 @@ impl<'db> Parameter<'db> {
601601
self.annotated_ty
602602
}
603603

604+
/// Kind of the parameter.
605+
pub(crate) fn kind(&self) -> &ParameterKind<'db> {
606+
&self.kind
607+
}
608+
604609
/// Name of the parameter (if it has one).
605610
pub(crate) fn name(&self) -> Option<&ast::name::Name> {
606611
match &self.kind {

0 commit comments

Comments
 (0)