Compare commits

...

2 Commits

Author SHA1 Message Date
Charlie Marsh
5634afc3c9 Try to improve generality 2025-12-23 23:12:08 -05:00
Charlie Marsh
fab8e34e07 [ty] Fix iteration over intersections with TypeVars whose bounds contain non-iterable types 2025-12-23 22:45:59 -05:00
2 changed files with 75 additions and 5 deletions

View File

@@ -951,3 +951,24 @@ for x in Bar:
# TODO: should reveal `Any`
reveal_type(x) # revealed: Unknown
```
## Iterating over an intersection with a TypeVar whose bound is a union
When a TypeVar has a union bound where some elements are iterable and some are not, and the TypeVar
is intersected with an iterable type (e.g., via `isinstance`), the iteration should use the iterable
parts of the TypeVar's bound.
```toml
[environment]
python-version = "3.12"
```
```py
def f[T: tuple[int, ...] | int](x: T):
if isinstance(x, tuple):
reveal_type(x) # revealed: T@f & tuple[object, ...]
for item in x:
# The TypeVar T is constrained to tuple[int, ...] by the isinstance check,
# so iterating should give `int`, not `object`.
reveal_type(item) # revealed: int
```

View File

@@ -6730,13 +6730,62 @@ impl<'db> Type<'db> {
// 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.
//
// For TypeVars, we replace them with their upper bound for iteration
// purposes. Once we are iterating, the fact that it's a TypeVar no
// longer matters: all that matters is the upper bound.
//
// For unions in an intersection context, if some elements are not
// iterable, we iterate only the iterable parts. This is sound because
// the intersection constrains the type to the iterable parts.
// For example, for `T & tuple[object, ...]` where `T: tuple[int, ...] | int`,
// iterating should give `int` (from the `tuple[int, ...]` part of T's bound),
// not `object` (from ignoring T entirely).
let try_iterate_element =
|element: Type<'db>| -> Option<Cow<'db, TupleSpec<'db>>> {
// Replace TypeVar with its upper bound for iteration purposes.
let element = if let Type::TypeVar(tvar) = element {
match tvar.typevar(db).bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound,
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.as_type(db)
}
None => return None,
}
} else {
element
};
// Try normal iteration.
if let Ok(spec) =
element.try_iterate_with_mode(db, EvaluationMode::Sync)
{
return Some(spec);
}
// For unions, try iterating only the iterable parts.
if let Type::Union(union) = element {
let mut iterable_specs = union.elements(db).iter().filter_map(
|elem| {
elem.try_iterate_with_mode(db, EvaluationMode::Sync).ok()
},
);
if let Some(first) = iterable_specs.next() {
let mut builder = TupleSpecBuilder::from(&*first);
for spec in iterable_specs {
builder = builder.union(db, &spec);
}
return Some(Cow::Owned(builder.build()));
}
}
None
};
let mut specs_iter = intersection
.positive_elements_or_object(db)
.filter_map(|element| {
element
.try_iterate_with_mode(db, EvaluationMode::Sync)
.ok()
});
.filter_map(try_iterate_element);
let first_spec = specs_iter.next()?;
let mut builder = TupleSpecBuilder::from(&*first_spec);
for spec in specs_iter {