From b6a99c9acfb9c491fe01b9ba381e80beb5312ef4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 13 Dec 2025 14:26:55 -0500 Subject: [PATCH 1/4] Respect intersections in iterations --- .../resources/mdtest/loops/for.md | 25 ++++++++++++ crates/ty_python_semantic/src/types.rs | 12 +++++- crates/ty_python_semantic/src/types/tuple.rs | 40 ++++++++++++++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index ed51e51c56..0da97ff5c0 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -337,6 +337,31 @@ for x in Test(): reveal_type(x) # revealed: int ``` +## Intersection type via isinstance narrowing + +When we have an intersection type via `isinstance` narrowing, we should be able to infer the +iterable element type precisely: + +```py +from typing import Sequence + +def _(x: Sequence[int], y: object): + reveal_type(x) # revealed: Sequence[int] + for item in x: + reveal_type(item) # revealed: int + + if isinstance(y, list): + reveal_type(y) # revealed: Top[list[Unknown]] + for item in y: + reveal_type(item) # revealed: object + + if isinstance(x, list): + reveal_type(x) # revealed: Sequence[int] & Top[list[Unknown]] + for item in x: + # int & object simplifies to int + reveal_type(item) # revealed: int +``` + ## Possibly-not-callable `__iter__` method ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1bbcc038cc..6bcc57f03f 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6612,6 +6612,17 @@ impl<'db> Type<'db> { None } } + Type::Intersection(intersection) => { + // For intersections, we iterate over each positive element and intersect + // the resulting element types. Negative elements don't affect iteration. + let mut elements_iter = intersection.positive_elements_or_object(db); + let first_element_spec = elements_iter.next()?.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?; + let mut builder = TupleSpecBuilder::from(&*first_element_spec); + for element in elements_iter { + builder = builder.intersect(db, &*element.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?); + } + Some(Cow::Owned(builder.build())) + } // N.B. These special cases aren't strictly necessary, they're just obvious optimizations Type::LiteralString | Type::Dynamic(_) => Some(Cow::Owned(TupleSpec::homogeneous(ty))), @@ -6634,7 +6645,6 @@ impl<'db> Type<'db> { | Type::SpecialForm(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) - | Type::Intersection(_) | Type::AlwaysTruthy | Type::AlwaysFalsy | Type::IntLiteral(_) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index f0cc3bedc8..e551871539 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -29,8 +29,8 @@ use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension}; use crate::types::generics::InferableTypeVars; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, - IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, - UnionBuilder, UnionType, + IntersectionType, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, + TypeRelation, UnionBuilder, UnionType, }; use crate::types::{Truthiness, TypeContext}; use crate::{Db, FxOrderSet, Program}; @@ -1787,6 +1787,42 @@ impl<'db> TupleSpecBuilder<'db> { } } + /// Return a new tuple-spec builder that reflects the intersection of this tuple and another tuple. + /// + /// For example, if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a + /// tuple-spec for `tuple[object, object]`, the result will be a tuple-spec builder for + /// `tuple[int, str]` (since `int & object` simplifies to `int`, and `str & object` to `str`). + /// + /// To keep things simple, we currently only attempt to preserve the "fixed-length-ness" of + /// a tuple spec if both `self` and `other` have the exact same length. For example, + /// if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a tuple-spec for + /// `tuple[int, str, bytes]`, the result will be a tuple-spec builder for + /// `tuple[int & str & bytes, ...]`. + pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { + match (&mut self, other) { + (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) + if our_elements.len() == new_elements.len() => + { + for (existing, new) in our_elements.iter_mut().zip(new_elements.elements()) { + *existing = IntersectionType::from_elements(db, [*existing, *new]); + } + self + } + + _ => { + let intersected = IntersectionType::from_elements( + db, + self.all_elements().chain(other.all_elements()), + ); + TupleSpecBuilder::Variable { + prefix: vec![], + variable: intersected, + suffix: vec![], + } + } + } + } + pub(super) fn build(self) -> TupleSpec<'db> { match self { TupleSpecBuilder::Fixed(elements) => { From 7bdce29a8f842cab3cc2ceb3b6f1b4baeac8e45a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 13 Dec 2025 15:12:20 -0500 Subject: [PATCH 2/4] Try to intersect types --- crates/ty_python_semantic/src/types/tuple.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index e551871539..ddaea1532c 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -1796,8 +1796,7 @@ impl<'db> TupleSpecBuilder<'db> { /// To keep things simple, we currently only attempt to preserve the "fixed-length-ness" of /// a tuple spec if both `self` and `other` have the exact same length. For example, /// if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a tuple-spec for - /// `tuple[int, str, bytes]`, the result will be a tuple-spec builder for - /// `tuple[int & str & bytes, ...]`. + /// `tuple[bytes]`, the result will be a tuple-spec builder for `tuple[(int | str) & bytes, ...]`. pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) @@ -1810,10 +1809,13 @@ impl<'db> TupleSpecBuilder<'db> { } _ => { - let intersected = IntersectionType::from_elements( - db, - self.all_elements().chain(other.all_elements()), - ); + // Each tuple yields the union of its elements when iterated. + // The intersection of two iterables yields elements that are + // in both, so we intersect their homogeneous element types. + let self_elements = UnionType::from_elements(db, self.all_elements()); + let other_elements = UnionType::from_elements(db, other.all_elements()); + let intersected = + IntersectionType::from_elements(db, [self_elements, other_elements]); TupleSpecBuilder::Variable { prefix: vec![], variable: intersected, From 99655b54c3b1c543928886917a4bd7815f2cea87 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 13 Dec 2025 15:53:15 -0500 Subject: [PATCH 3/4] Revert "Try to intersect types" This reverts commit 7bdce29a8f842cab3cc2ceb3b6f1b4baeac8e45a. --- crates/ty_python_semantic/src/types/tuple.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index ddaea1532c..e551871539 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -1796,7 +1796,8 @@ impl<'db> TupleSpecBuilder<'db> { /// To keep things simple, we currently only attempt to preserve the "fixed-length-ness" of /// a tuple spec if both `self` and `other` have the exact same length. For example, /// if `self` is a tuple-spec builder for `tuple[int, str]` and `other` is a tuple-spec for - /// `tuple[bytes]`, the result will be a tuple-spec builder for `tuple[(int | str) & bytes, ...]`. + /// `tuple[int, str, bytes]`, the result will be a tuple-spec builder for + /// `tuple[int & str & bytes, ...]`. pub(crate) fn intersect(mut self, db: &'db dyn Db, other: &TupleSpec<'db>) -> Self { match (&mut self, other) { (TupleSpecBuilder::Fixed(our_elements), TupleSpec::Fixed(new_elements)) @@ -1809,13 +1810,10 @@ impl<'db> TupleSpecBuilder<'db> { } _ => { - // Each tuple yields the union of its elements when iterated. - // The intersection of two iterables yields elements that are - // in both, so we intersect their homogeneous element types. - let self_elements = UnionType::from_elements(db, self.all_elements()); - let other_elements = UnionType::from_elements(db, other.all_elements()); - let intersected = - IntersectionType::from_elements(db, [self_elements, other_elements]); + let intersected = IntersectionType::from_elements( + db, + self.all_elements().chain(other.all_elements()), + ); TupleSpecBuilder::Variable { prefix: vec![], variable: intersected, From d1ceb75960374c3682e57b77df5346cd7d949640 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 13 Dec 2025 17:09:24 -0500 Subject: [PATCH 4/4] Change error conditions --- .../resources/mdtest/loops/for.md | 39 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 18 ++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/loops/for.md b/crates/ty_python_semantic/resources/mdtest/loops/for.md index 0da97ff5c0..1d657575c6 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/for.md @@ -362,6 +362,45 @@ def _(x: Sequence[int], y: object): reveal_type(item) # revealed: int ``` +## Intersection where some elements are not iterable + +When iterating over an intersection type, we should only fail if all positive elements fail to +iterate. If some elements are iterable and some are not, we should iterate over the iterable ones +and intersect their element types. + +```py +from ty_extensions import Intersection + +class NotIterable: + pass + +def _(x: Intersection[list[int], NotIterable]): + # `list[int]` is iterable (yielding `int`), but `NotIterable` is not. + # We should still be able to iterate over the intersection. + for item in x: + reveal_type(item) # revealed: int +``` + +## Intersection where all elements are not iterable + +When iterating over an intersection type where all positive elements are not iterable, we should +fail to iterate. + +```py +from ty_extensions import Intersection + +class NotIterable1: + pass + +class NotIterable2: + pass + +def _(x: Intersection[NotIterable1, NotIterable2]): + # error: [not-iterable] + for item in x: + reveal_type(item) # revealed: Unknown +``` + ## Possibly-not-callable `__iter__` method ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6bcc57f03f..233095d0be 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6615,11 +6615,19 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => { // For intersections, we iterate over each positive element and intersect // the resulting element types. Negative elements don't affect iteration. - let mut elements_iter = intersection.positive_elements_or_object(db); - let first_element_spec = elements_iter.next()?.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?; - let mut builder = TupleSpecBuilder::from(&*first_element_spec); - for element in elements_iter { - builder = builder.intersect(db, &*element.try_iterate_with_mode(db, EvaluationMode::Sync).ok()?); + // We only fail if all elements fail to iterate; as long as at least one + // element can be iterated over, we can produce a result. + let mut specs_iter = intersection + .positive_elements_or_object(db) + .filter_map(|element| { + element + .try_iterate_with_mode(db, EvaluationMode::Sync) + .ok() + }); + let first_spec = specs_iter.next()?; + let mut builder = TupleSpecBuilder::from(&*first_spec); + for spec in specs_iter { + builder = builder.intersect(db, &spec); } Some(Cow::Owned(builder.build())) }