From 807a8a7a2902013d9367c6dbd825dbf23e896a3a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 15 Apr 2025 16:34:07 -0400 Subject: [PATCH] [red-knot] Acknowledge that `T & anything` is assignable to `T` (#17413) This reworks the assignability/subtyping relations a bit to handle typevars better: 1. For the most part, types are not assignable to typevars, since there's no guarantee what type the typevar will be specialized to. 2. An intersection is an exception, if it contains the typevar itself as one of the positive elements. This should fall out from the other clauses automatically, since a typevar is assignable to itself, and an intersection is assignable to something if any positive element is assignable to that something. 3. Constrained typevars are an exception, since they must be specialized to _exactly_ one of the constraints, not to a _subtype_ of a constraint. If a type is assignable to every constraint, then the type is also assignable to the constrained typevar. We already had a special case for (3), but the ordering of it relative to the intersection clauses meant we weren't catching (2) correctly. To fix this, we keep the special case for (3), but fall through to the other match arms for non-constrained typevars and if the special case isn't true for a constrained typevar. Closes https://github.com/astral-sh/ruff/issues/17364 --- .../resources/mdtest/generics/pep695.md | 14 ++++ crates/red_knot_python_semantic/src/types.rs | 72 ++++++++++--------- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md index 4a9198885f..907e18c307 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md @@ -507,6 +507,20 @@ def remove_constraint[T: (int, str, bool)](t: T) -> None: reveal_type(x) # revealed: T & Any ``` +The intersection of a typevar with any other type is assignable to (and if fully static, a subtype +of) itself. + +```py +from knot_extensions import is_assignable_to, is_subtype_of, static_assert, Not + +def intersection_is_assignable[T](t: T) -> None: + static_assert(is_assignable_to(Intersection[T, None], T)) + static_assert(is_assignable_to(Intersection[T, Not[None]], T)) + + static_assert(is_subtype_of(Intersection[T, None], T)) + static_assert(is_subtype_of(Intersection[T, Not[None]], T)) +``` + ## Narrowing We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar: diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4235103bee..2db902bb34 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -890,23 +890,18 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| self.is_subtype_of(db, elem_ty)), - (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { - // No types are a subtype of a bounded typevar, or of an unbounded unconstrained - // typevar, since there's no guarantee what type the typevar will be specialized - // to. If the typevar is bounded, it might be specialized to a smaller type than - // the bound. (This is true even if the bound is a final class, since the typevar - // can still be specialized to `Never`.) - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - // If the typevar is constrained, there must be multiple constraints, and the - // typevar might be specialized to any one of them. However, the constraints do not - // have to be disjoint, which means an lhs type might be a subtype of all of the - // constraints. - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| self.is_subtype_of(db, *constraint)), - }, + // If the typevar is constrained, there must be multiple constraints, and the typevar + // might be specialized to any one of them. However, the constraints do not have to be + // disjoint, which means an lhs type might be a subtype of all of the constraints. + (_, Type::TypeVar(typevar)) + if typevar.constraints(db).is_some_and(|constraints| { + constraints + .iter() + .all(|constraint| self.is_subtype_of(db, *constraint)) + }) => + { + true + } // If both sides are intersections we need to handle the right side first // (A & B & C) is a subtype of (A & B) because the left is a subtype of both A and B, @@ -927,6 +922,13 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| elem_ty.is_subtype_of(db, target)), + // Other than the special cases checked above, no other types are a subtype of a + // typevar, since there's no guarantee what type the typevar will be specialized to. + // (If the typevar is bounded, it might be specialized to a smaller type than the + // bound. This is true even if the bound is a final class, since the typevar can still + // be specialized to `Never`.) + (_, Type::TypeVar(_)) => false, + // Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`. // If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively. (left, Type::AlwaysFalsy) => left.bool(db).is_always_false(), @@ -1186,23 +1188,18 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)), - (_, Type::TypeVar(typevar)) => match typevar.bound_or_constraints(db) { - // No types are assignable to a bounded typevar, or to an unbounded unconstrained - // typevar, since there's no guarantee what type the typevar will be specialized - // to. If the typevar is bounded, it might be specialized to a smaller type than - // the bound. (This is true even if the bound is a final class, since the typevar - // can still be specialized to `Never`.) - None => false, - Some(TypeVarBoundOrConstraints::UpperBound(_)) => false, - // If the typevar is constrained, there must be multiple constraints, and the - // typevar might be specialized to any one of them. However, the constraints do not - // have to be disjoint, which means an lhs type might be assignable to all of the - // constraints. - Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints - .elements(db) - .iter() - .all(|constraint| self.is_assignable_to(db, *constraint)), - }, + // If the typevar is constrained, there must be multiple constraints, and the typevar + // might be specialized to any one of them. However, the constraints do not have to be + // disjoint, which means an lhs type might be assignable to all of the constraints. + (_, Type::TypeVar(typevar)) + if typevar.constraints(db).is_some_and(|constraints| { + constraints + .iter() + .all(|constraint| self.is_assignable_to(db, *constraint)) + }) => + { + true + } // If both sides are intersections we need to handle the right side first // (A & B & C) is assignable to (A & B) because the left is assignable to both A and B, @@ -1230,6 +1227,13 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| elem_ty.is_assignable_to(db, ty)), + // Other than the special cases checked above, no other types are assignable to a + // typevar, since there's no guarantee what type the typevar will be specialized to. + // (If the typevar is bounded, it might be specialized to a smaller type than the + // bound. This is true even if the bound is a final class, since the typevar can still + // be specialized to `Never`.) + (_, Type::TypeVar(_)) => false, + // A tuple type S is assignable to a tuple type T if their lengths are the same, and // each element of S is assignable to the corresponding element of T. (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => {