Skip to content

Commit c5b4ee6

Browse files
authored
[ty] Support solving generics involving PEP 695 type aliases (#22678)
## Summary Fixes type variable inference when a PEP 695 type alias is used as a function parameter. **Before:** ```python type MyList[T] = list[T] def head[T](my_list: MyList[T]) -> T: ... reveal_type(head([1, 2])) # Unknown ``` **After:** Correctly infers `int` by expanding `MyList[T]` to `list[T]` during solving. Changes in `generics.rs`: - **Early formal expansion**: Expand parameter type aliases immediately so underlying structure is visible for structural matching - **Late actual expansion**: Expand argument type aliases after direct matching attempts to preserve alias identity for `reveal_type()` and literal promotion - **Union induction**: Recursively match formal type against each union element to support nested aliases like `ListOfPairs[T] = list[Pair[T]]` The early/late expansion asymmetry is intentional: formal needs structural visibility; actual needs identity preservation. ## Test Plan - New tests in `aliases.md` covering simple, nested, and union alias cases - All 313 existing mdtests pass Closes astral-sh/ty#1851.
1 parent b9a6129 commit c5b4ee6

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,37 @@ def _(x: DivergentList[int]):
328328
d1: DivergentList[int] = [x]
329329
d2: DivergentList[int] = x[0]
330330
```
331+
332+
## Solving generics with type alias parameters
333+
334+
A generic function parameter annotated with a PEP 695 type alias that contains a type variable
335+
should properly infer the specialization from the argument:
336+
337+
```py
338+
type MyList[T] = list[T]
339+
type MyDict[K, V] = dict[K, V]
340+
341+
def head[T](my_list: MyList[T]) -> T:
342+
return my_list[0]
343+
344+
def get_value[K, V](my_dict: MyDict[K, V], key: K) -> V:
345+
return my_dict[key]
346+
347+
reveal_type(head([1, 2])) # revealed: int
348+
reveal_type(head(["a", "b"])) # revealed: str
349+
350+
d: dict[str, int] = {"a": 1}
351+
reveal_type(get_value(d, "a")) # revealed: int
352+
```
353+
354+
It also works in the reverse direction, where the type alias is used as the argument type:
355+
356+
```py
357+
type MyList[T] = list[T]
358+
359+
def head[T](l: list[T]) -> T:
360+
return l[0]
361+
362+
def _(x: MyList[int]):
363+
reveal_type(head(x)) # revealed: int
364+
```

crates/ty_python_semantic/src/types/generics.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,6 +1769,12 @@ impl<'db> SpecializationBuilder<'db> {
17691769
let formal = formal.filter_disjoint_elements(self.db, actual, self.inferable);
17701770

17711771
match (formal, actual) {
1772+
// Expand PEP 695 type aliases in the formal type.
1773+
// This is necessary for solving generics like `def head[T](my_list: MyList[T]) -> T`.
1774+
(Type::TypeAlias(alias), _) => {
1775+
return self.infer_map_impl(alias.value_type(self.db), actual, polarity, f, seen);
1776+
}
1777+
17721778
// TODO: We haven't implemented a full unification solver yet. If typevars appear in
17731779
// multiple union elements, we ideally want to express that _only one_ of them needs to
17741780
// match, and that we should infer the smallest type mapping that allows that.
@@ -2100,6 +2106,15 @@ impl<'db> SpecializationBuilder<'db> {
21002106
}
21012107
}
21022108

2109+
// Expand type aliases in the actual type.
2110+
//
2111+
// This is placed at the end of the match block to avoid expanding the type alias
2112+
// when it can be matched directly against a type variable in the formal type,
2113+
// e.g., `reveal_type(alias)` should reveal the type alias, not its value type.
2114+
(formal, Type::TypeAlias(alias)) => {
2115+
return self.infer_map_impl(formal, alias.value_type(self.db), polarity, f, seen);
2116+
}
2117+
21032118
// TODO: Add more forms that we can structurally induct into: type[C], callables
21042119
_ => {}
21052120
}

0 commit comments

Comments
 (0)