Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,40 @@ A = TypeAliasType("A", "list[Without[object, A] | A]")
def f(x: A):
assert_type(x[0], object)
```

## Recursive tuples intersected with a protocol

Specialization can put a recursive tuple alias inside a positive intersection. The tuple and its
elements retain that alias, so they can be inspected without repeatedly unfolding the tuple.

```py
from collections.abc import Iterator
from typing import Any, Protocol, TypeVar
from typing_extensions import TypeAliasType, assert_type
from ty_extensions import Intersection

class Items(Protocol):
def __iter__(self) -> Iterator[Any]: ...

T = TypeVar("T")
WithItems = TypeAliasType("WithItems", Intersection[Items, T], type_params=(T,))
A = TypeAliasType("A", "tuple[WithItems[A], ...]")

def f(x: A):
reveal_type(x) # revealed: tuple[WithItems[A], ...]
assert_type(x[0], WithItems[A])
y: int = x[0] # error: [invalid-assignment]
```

The standard `Iterable` protocol exercises the same behavior with its typeshed definition.

```py
from collections.abc import Iterable

WithIterable = TypeAliasType("WithIterable", Intersection[Iterable[Any], T], type_params=(T,))
B = TypeAliasType("B", "tuple[WithIterable[B], ...]")

def g(x: B):
reveal_type(x) # revealed: tuple[WithIterable[B], ...]
assert_type(x[0], WithIterable[B])
```
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,41 @@ def f(x: A):
assert_type(x[0], object)
```

## Recursive tuples intersected with a protocol

Specialization can put a recursive tuple alias inside a positive intersection. The tuple and its
elements retain that alias, so they can be inspected without repeatedly unfolding the tuple.

```py
from collections.abc import Iterator
from typing import Any, Protocol, assert_type
from ty_extensions import Intersection

class Items(Protocol):
def __iter__(self) -> Iterator[Any]: ...

type WithItems[T] = Intersection[Items, T]
type A = tuple[WithItems[A], ...]

def f(x: A):
reveal_type(x) # revealed: tuple[WithItems[A], ...]
assert_type(x[0], WithItems[A])
y: int = x[0] # error: [invalid-assignment]
```

The standard `Iterable` protocol exercises the same behavior with its typeshed definition.

```py
from collections.abc import Iterable

type WithIterable[T] = Intersection[Iterable[Any], T]
type B = tuple[WithIterable[B], ...]

def g(x: B):
reveal_type(x) # revealed: tuple[WithIterable[B], ...]
assert_type(x[0], WithIterable[B])
```

## Solving generics with type alias parameters

A generic function parameter annotated with a PEP 695 type alias that contains a type variable
Expand Down
3 changes: 1 addition & 2 deletions crates/ty_python_semantic/src/types/set_theoretic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,8 +1135,7 @@ impl<'db> IntersectionType<'db> {
}
Type::Intersection(IntersectionType::new(db, positive, negative))
} else {
let mut builder =
IntersectionBuilder::new(db, visitor.env).preserve_negated_aliases(true);
let mut builder = IntersectionBuilder::new(db, visitor.env).preserve_aliases(true);
for positive in self.positive(db) {
builder.add_positive_in_place(positive.apply_type_mapping_impl(
db,
Expand Down
31 changes: 21 additions & 10 deletions crates/ty_python_semantic/src/types/set_theoretic/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,7 @@ pub(crate) struct IntersectionBuilder<'db> {
// One disjunction does not multiply alternatives. Only subsequent distributions consume
// the bounded constructor's budget, after impossible and redundant branches are removed.
has_disjunction: bool,
preserve_negated_aliases: bool,
preserve_aliases: bool,
}

impl<'db> IntersectionBuilder<'db> {
Expand All @@ -1326,17 +1326,19 @@ impl<'db> IntersectionBuilder<'db> {
env: env.clone(),
intersections: vec![InnerIntersectionBuilder::default()],
has_disjunction: false,
preserve_negated_aliases: false,
preserve_aliases: false,
}
}

/// Retain aliases under negation when their values need no further normalization.
/// Retain aliases when rebuilding mapped intersections.
///
/// Rebuilding a mapped intersection must not unfold a recursive alias on every visit.
/// Aliases to set-theoretic types, `object`, `Never`, and other types with special
/// negation rules still need to be expanded.
pub(crate) fn preserve_negated_aliases(mut self, preserve: bool) -> Self {
self.preserve_negated_aliases = preserve;
/// Unfolding aliases after their mapping has completed can repeatedly insert a recursive
/// type into itself. Positive elements retain aliases to non-`object` nominal instances.
/// Negative elements retain aliases whose values need no special negation rules.
/// In both cases we still expand aliases to set operations or `object` so their
/// simplifications remain visible.
pub(crate) fn preserve_aliases(mut self, preserve: bool) -> Self {
self.preserve_aliases = preserve;
self
}

Expand Down Expand Up @@ -1484,7 +1486,16 @@ impl<'db> IntersectionBuilder<'db> {
}
seen_aliases.push(ty);
let value_type = ty.resolve_type_alias(db);
self.add_positive_impl::<L>(value_type, seen_aliases)?;
if self.preserve_aliases
&& matches!(ty, Type::TypeAlias(_))
&& matches!(value_type, Type::NominalInstance(instance) if !instance.is_object())
{
for inner in &mut self.intersections {
inner.add_positive(db, &self.env, ty);
}
} else {
self.add_positive_impl::<L>(value_type, seen_aliases)?;
}
}
Type::Union(union) => {
// Distribute ourself over this union: for each union element, clone ourself and
Expand Down Expand Up @@ -1542,7 +1553,7 @@ impl<'db> IntersectionBuilder<'db> {
/// of its resolved value. Alias chains and set-theoretic types need expansion here;
/// the remaining exclusions have special rules in `InnerIntersectionBuilder::add_negative`.
fn should_preserve_negated_alias(&self, ty: Type<'db>, value_type: Type<'db>) -> bool {
if !self.preserve_negated_aliases || !matches!(ty, Type::TypeAlias(_)) {
if !self.preserve_aliases || !matches!(ty, Type::TypeAlias(_)) {
return false;
}

Expand Down
Loading