[red-knot] Heterogeneous tuple types with differently ordered (but equivalent) unions at the same index should be considered equivalent (#15637)

This commit is contained in:
Alex Waygood
2025-01-21 12:51:20 +00:00
committed by GitHub
parent 023c52d82b
commit 187a358d7a
2 changed files with 43 additions and 12 deletions

View File

@@ -66,4 +66,22 @@ static_assert(is_equivalent_to(Intersection[Q, R, Not[P]], Intersection[Not[P],
static_assert(is_equivalent_to(Intersection[Q | R, Not[P | S]], Intersection[Not[S | P], R | Q]))
```
## Tuples containing equivalent but differently ordered unions/intersections are equivalent
```py
from knot_extensions import is_equivalent_to, TypeOf, static_assert, Intersection, Not
from typing import Literal
class P: ...
class Q: ...
class R: ...
class S: ...
static_assert(is_equivalent_to(tuple[P | Q], tuple[Q | P]))
static_assert(is_equivalent_to(tuple[P | None], tuple[None | P]))
static_assert(
is_equivalent_to(tuple[Intersection[P, Q] | Intersection[R, Not[S]]], tuple[Intersection[Not[S], R] | Intersection[Q, P]])
)
```
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent

View File

@@ -1,5 +1,4 @@
use std::hash::Hash;
use std::iter;
use bitflags::bitflags;
use context::InferContext;
@@ -1123,6 +1122,7 @@ impl<'db> Type<'db> {
(Type::Intersection(left), Type::Intersection(right)) => {
left.is_equivalent_to(db, right)
}
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
_ => self.is_fully_static(db) && other.is_fully_static(db) && self == other,
}
}
@@ -1158,12 +1158,11 @@ impl<'db> Type<'db> {
///
/// [Summary of type relations]: https://typing.readthedocs.io/en/latest/spec/concepts.html#summary-of-type-relations
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Type<'db>) -> bool {
let equivalent =
|(first, second): (&Type<'db>, &Type<'db>)| first.is_gradual_equivalent_to(db, *second);
if self == other {
return true;
}
match (self, other) {
(_, _) if self == other => true,
(Type::Dynamic(_), Type::Dynamic(_)) => true,
(Type::SubclassOf(first), Type::SubclassOf(second)) => {
@@ -1174,13 +1173,7 @@ impl<'db> Type<'db> {
}
}
(Type::Tuple(first), Type::Tuple(second)) => {
let first_elements = first.elements(db);
let second_elements = second.elements(db);
first_elements.len() == second_elements.len()
&& iter::zip(first_elements, second_elements).all(equivalent)
}
(Type::Tuple(first), Type::Tuple(second)) => first.is_gradual_equivalent_to(db, second),
(Type::Union(first), Type::Union(second)) => first.is_gradual_equivalent_to(db, second),
@@ -4588,6 +4581,26 @@ impl<'db> TupleType<'db> {
Type::Tuple(Self::new(db, elements.into_boxed_slice()))
}
pub fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_elements = self.elements(db);
let other_elements = other.elements(db);
self_elements.len() == other_elements.len()
&& self_elements
.iter()
.zip(other_elements)
.all(|(self_ty, other_ty)| self_ty.is_equivalent_to(db, *other_ty))
}
pub fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
let self_elements = self.elements(db);
let other_elements = other.elements(db);
self_elements.len() == other_elements.len()
&& self_elements
.iter()
.zip(other_elements)
.all(|(self_ty, other_ty)| self_ty.is_gradual_equivalent_to(db, *other_ty))
}
pub fn get(&self, db: &'db dyn Db, index: usize) -> Option<Type<'db>> {
self.elements(db).get(index).copied()
}