Skip to content

Commit dd5b02a

Browse files
authored
[red-knot] Fix gradual equivalence for callable types (#16887)
## Summary As mentioned in #16698 (comment), part of #15382, this PR updates the `is_gradual_equivalent_to` implementation between callable types to be similar to `is_equivalent_to` and checks other attributes of parameters like name, optionality, and parameter kind. ## Test Plan Expand the existing test cases to consider other properties but not all similar to how the tests are structured for subtyping and assignability.
1 parent 68ea2b8 commit dd5b02a

File tree

2 files changed

+67
-60
lines changed

2 files changed

+67
-60
lines changed

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
6868

6969
## Callable
7070

71+
The examples provided below are only a subset of the possible cases and only include the ones with
72+
gradual types. The cases with fully static types and using different combinations of parameter kinds
73+
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
74+
7175
```py
7276
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
7377
from typing import Any, Callable
@@ -94,7 +98,7 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[]
9498
And, similarly for parameters with no annotations.
9599

96100
```py
97-
def f2(a, b) -> None:
101+
def f2(a, b, /) -> None:
98102
return
99103

100104
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
@@ -115,8 +119,8 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without
115119
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
116120
```
117121

118-
But, a function with either `*args` or `**kwargs` is not gradual equivalent to a callable with `...`
119-
as the parameter type.
122+
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
123+
callable with `...` as the parameter type.
120124

121125
```py
122126
def variadic_args(*args):
@@ -129,4 +133,25 @@ static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_arg
129133
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
130134
```
131135

136+
Parameter names, default values, and it's kind should also be considered when checking for gradual
137+
equivalence.
138+
139+
```py
140+
def f1(a): ...
141+
def f2(b): ...
142+
143+
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
144+
145+
def f3(a=1): ...
146+
def f4(a=2): ...
147+
def f5(a): ...
148+
149+
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
150+
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5]))
151+
152+
def f6(a, /): ...
153+
154+
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6]))
155+
```
156+
132157
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize

crates/red_knot_python_semantic/src/types.rs

Lines changed: 39 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4346,32 +4346,52 @@ impl<'db> GeneralCallableType<'db> {
43464346
.is_some_and(|return_type| return_type.is_fully_static(db))
43474347
}
43484348

4349+
/// Return `true` if `self` has exactly the same set of possible static materializations as
4350+
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
4351+
/// `other`).
4352+
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
4353+
self.is_equivalent_to_impl(db, other, |self_type, other_type| {
4354+
self_type
4355+
.unwrap_or(Type::unknown())
4356+
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
4357+
})
4358+
}
4359+
43494360
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`.
43504361
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
4362+
self.is_equivalent_to_impl(db, other, |self_type, other_type| {
4363+
match (self_type, other_type) {
4364+
(Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type),
4365+
// We need the catch-all case here because it's not guaranteed that this is a fully
4366+
// static type.
4367+
_ => false,
4368+
}
4369+
})
4370+
}
4371+
4372+
/// Implementation for the [`is_equivalent_to`] and [`is_gradual_equivalent_to`] for callable
4373+
/// types.
4374+
///
4375+
/// [`is_equivalent_to`]: Self::is_equivalent_to
4376+
/// [`is_gradual_equivalent_to`]: Self::is_gradual_equivalent_to
4377+
fn is_equivalent_to_impl<F>(self, db: &'db dyn Db, other: Self, check_types: F) -> bool
4378+
where
4379+
F: Fn(Option<Type<'db>>, Option<Type<'db>>) -> bool,
4380+
{
43514381
let self_signature = self.signature(db);
43524382
let other_signature = other.signature(db);
43534383

4384+
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
4385+
// parameters because it is internally represented by adding `*Any` and `**Any` to the
4386+
// parameter list.
43544387
let self_parameters = self_signature.parameters();
43554388
let other_parameters = other_signature.parameters();
43564389

43574390
if self_parameters.len() != other_parameters.len() {
43584391
return false;
43594392
}
43604393

4361-
if self_parameters.is_gradual() || other_parameters.is_gradual() {
4362-
return false;
4363-
}
4364-
4365-
// Check equivalence relationship between two optional types. If either of them is `None`,
4366-
// then it is not a fully static type which means it's not equivalent either.
4367-
let is_equivalent = |self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| match (
4368-
self_type, other_type,
4369-
) {
4370-
(Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type),
4371-
_ => false,
4372-
};
4373-
4374-
if !is_equivalent(self_signature.return_ty, other_signature.return_ty) {
4394+
if !check_types(self_signature.return_ty, other_signature.return_ty) {
43754395
return false;
43764396
}
43774397

@@ -4419,7 +4439,7 @@ impl<'db> GeneralCallableType<'db> {
44194439
_ => return false,
44204440
}
44214441

4422-
if !is_equivalent(
4442+
if !check_types(
44234443
self_parameter.annotated_type(),
44244444
other_parameter.annotated_type(),
44254445
) {
@@ -4430,48 +4450,6 @@ impl<'db> GeneralCallableType<'db> {
44304450
true
44314451
}
44324452

4433-
/// Return `true` if `self` has exactly the same set of possible static materializations as
4434-
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
4435-
/// `other`).
4436-
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
4437-
let self_signature = self.signature(db);
4438-
let other_signature = other.signature(db);
4439-
4440-
if self_signature.parameters().len() != other_signature.parameters().len() {
4441-
return false;
4442-
}
4443-
4444-
// Check gradual equivalence between the two optional types. In the context of a callable
4445-
// type, the `None` type represents an `Unknown` type.
4446-
let are_optional_types_gradually_equivalent =
4447-
|self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| {
4448-
self_type
4449-
.unwrap_or(Type::unknown())
4450-
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
4451-
};
4452-
4453-
if !are_optional_types_gradually_equivalent(
4454-
self_signature.return_ty,
4455-
other_signature.return_ty,
4456-
) {
4457-
return false;
4458-
}
4459-
4460-
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
4461-
// parameters because it is internally represented by adding `*Any` and `**Any` to the
4462-
// parameter list.
4463-
self_signature
4464-
.parameters()
4465-
.iter()
4466-
.zip(other_signature.parameters().iter())
4467-
.all(|(self_param, other_param)| {
4468-
are_optional_types_gradually_equivalent(
4469-
self_param.annotated_type(),
4470-
other_param.annotated_type(),
4471-
)
4472-
})
4473-
}
4474-
44754453
/// Return `true` if `self` is assignable to `other`.
44764454
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
44774455
self.is_assignable_to_impl(db, other, |type1, type2| {
@@ -4483,6 +4461,10 @@ impl<'db> GeneralCallableType<'db> {
44834461
}
44844462

44854463
/// Return `true` if `self` is a subtype of `other`.
4464+
///
4465+
/// # Panics
4466+
///
4467+
/// Panics if `self` or `other` is not a fully static type.
44864468
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
44874469
self.is_assignable_to_impl(db, other, |type1, type2| {
44884470
// SAFETY: Subtype relation is only checked for fully static types.

0 commit comments

Comments
 (0)