Skip to content

Commit 26676b7

Browse files
committed
[red-knot] Check gradual equivalence between callable types
1 parent da069aa commit 26676b7

File tree

2 files changed

+70
-0
lines changed

2 files changed

+70
-0
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,22 @@ 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, 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[[Unknown, 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+
# This mimics the behavior when a return type is given for one of the callables and not the other.
80+
static_assert(not is_gradual_equivalent_to(Callable[..., None], Callable[..., Unknown]))
81+
```
82+
6583
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize

crates/red_knot_python_semantic/src/types.rs

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

960+
(
961+
Type::Callable(CallableType::General(first)),
962+
Type::Callable(CallableType::General(second)),
963+
) => first.is_gradual_equivalent_to(db, second),
964+
960965
_ => false,
961966
}
962967
}
@@ -4377,6 +4382,53 @@ impl<'db> GeneralCallableType<'db> {
43774382
Signature::new(Parameters::unknown(), Some(Type::unknown())),
43784383
)
43794384
}
4385+
4386+
/// Return `true` if `self` has exactly the same set of possible static materializations as
4387+
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
4388+
/// `other`).
4389+
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
4390+
let self_signature = self.signature(db);
4391+
let other_signature = other.signature(db);
4392+
4393+
if self_signature.parameters().is_gradual() != other_signature.parameters().is_gradual() {
4394+
return false;
4395+
}
4396+
4397+
if self_signature.parameters().len() != other_signature.parameters().len() {
4398+
return false;
4399+
}
4400+
4401+
// Check gradual equivalence between the two optional types. They are gradual equivalent
4402+
// if they are both `None` or both the Some types are gradual equivalent.
4403+
let are_optional_types_gradually_equivalent =
4404+
|self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| match (
4405+
self_type, other_type,
4406+
) {
4407+
(Some(self_type), Some(other_type)) => {
4408+
self_type.is_gradual_equivalent_to(db, other_type)
4409+
}
4410+
(None, None) => true,
4411+
_ => false,
4412+
};
4413+
4414+
if !are_optional_types_gradually_equivalent(
4415+
self_signature.return_ty,
4416+
other_signature.return_ty,
4417+
) {
4418+
return false;
4419+
}
4420+
4421+
self_signature
4422+
.parameters()
4423+
.iter()
4424+
.zip(other_signature.parameters().iter())
4425+
.all(|(self_param, other_param)| {
4426+
are_optional_types_gradually_equivalent(
4427+
self_param.annotated_type(),
4428+
other_param.annotated_type(),
4429+
)
4430+
})
4431+
}
43804432
}
43814433

43824434
/// A type that represents callable objects.

0 commit comments

Comments
 (0)