This commit is contained in:
Charlie Marsh 2025-12-16 16:37:01 -05:00 committed by GitHub
commit af6240e7ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 121 additions and 3 deletions

View File

@ -337,6 +337,70 @@ for x in Test():
reveal_type(x) # revealed: int 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
```
## 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 ## Possibly-not-callable `__iter__` method
```py ```py

View File

@ -6695,6 +6695,25 @@ impl<'db> Type<'db> {
None None
} }
} }
Type::Intersection(intersection) => {
// For intersections, we iterate over each positive element and intersect
// the resulting element types. Negative elements don't affect iteration.
// 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()))
}
// N.B. These special cases aren't strictly necessary, they're just obvious optimizations // N.B. These special cases aren't strictly necessary, they're just obvious optimizations
Type::LiteralString | Type::Dynamic(_) => Some(Cow::Owned(TupleSpec::homogeneous(ty))), Type::LiteralString | Type::Dynamic(_) => Some(Cow::Owned(TupleSpec::homogeneous(ty))),
@ -6717,7 +6736,6 @@ impl<'db> Type<'db> {
| Type::SpecialForm(_) | Type::SpecialForm(_)
| Type::KnownInstance(_) | Type::KnownInstance(_)
| Type::PropertyInstance(_) | Type::PropertyInstance(_)
| Type::Intersection(_)
| Type::AlwaysTruthy | Type::AlwaysTruthy
| Type::AlwaysFalsy | Type::AlwaysFalsy
| Type::IntLiteral(_) | Type::IntLiteral(_)

View File

@ -29,8 +29,8 @@ use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
use crate::types::generics::InferableTypeVars; use crate::types::generics::InferableTypeVars;
use crate::types::{ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, IntersectionType, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping,
UnionBuilder, UnionType, TypeRelation, UnionBuilder, UnionType,
}; };
use crate::types::{Truthiness, TypeContext}; use crate::types::{Truthiness, TypeContext};
use crate::{Db, FxOrderSet, Program}; 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> { pub(super) fn build(self) -> TupleSpec<'db> {
match self { match self {
TupleSpecBuilder::Fixed(elements) => { TupleSpecBuilder::Fixed(elements) => {