Description
Summary
Currently, this mdtest fails, even though it should pass:
def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ...
def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ...
static_assert(is_equivalent_to(CallableTypeOf[f3] | bool | CallableTypeOf[f4], CallableTypeOf[f4] | bool | CallableTypeOf[f3]))
Note that this is despite the fact that the simpler assertion static_assert(is_equivalent_to(CallableTypeOf[f3], CallableTypeOf[f4])
passes: the Callable
types must appear in unions for the bug to occur.
I hoped that applying this diff to type_ordering.rs
would fix this bug, but the assertion still fails:
diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs
index 79f0abe1d..c0b160036 100644
--- a/crates/red_knot_python_semantic/src/types/type_ordering.rs
+++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs
@@ -83,9 +83,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
- (Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
- Ordering::Equal
- }
+ (
+ Type::Callable(CallableType::General(left)),
+ Type::Callable(CallableType::General(right)),
+ ) => left.cmp(right),
(Type::Callable(CallableType::General(_)), _) => Ordering::Less,
(_, Type::Callable(CallableType::General(_))) => Ordering::Greater,
Adding some reveal_type
calls to the test indicates why: the two unions have different Callable
types in them (even though the two Callable
types are equivalent, they have different names for the parameters):
def f3(a1: int, /, *args1: int, **kwargs2: int) -> None: ...
def f4(a2: int, /, *args2: int, **kwargs1: int) -> None: ...
def f(x: CallableTypeOf[f3] | bool | CallableTypeOf[f4], y: CallableTypeOf[f4] | bool | CallableTypeOf[f3]):
reveal_type(x) # revealed: ((a1: int, /, *args1: int, **kwargs2: int) -> None) | bool
reveal_type(y) # revealed: ((a2: int, /, *args2: int, **kwargs1: int) -> None) | bool
and our implementation of is_equivalent_to
for UnionType
s currently assumes that two sorted unions can only ever be equivalent if they have identical Salsa IDs:
ruff/crates/red_knot_python_semantic/src/types.rs
Lines 5081 to 5116 in ab1011c
There are two possible fixes here:
- Change things so that two equivalent
Callable
types are always guaranteed to have the same Salsa IDs (discussed as a possibility in [red-knot] Check whether two callable types are equivalent #16698 (comment)) - Change the implementation of
UnionType::is_equivalent_to
so that it looks more similar toUnionType::is_gradual_equivalent_to
, which takes care to iterate over all elements rather than simply checking the Salsa IDs:ruff/crates/red_knot_python_semantic/src/types.rs
Lines 5144 to 5148 in ab1011c