Skip to content
Merged
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 @@ -386,6 +386,95 @@ def singleton(flag: bool = False) -> Callable[[Callable[[int], S]], Callable[[in
return wrapper
```

## Return type inference from partially annotated overloads

The catch-all overload returns `object`, which is preserved when inferring a return type from the
whole callback even though the literal-specific overloads have unannotated return types.

```py
from typing import Callable, Literal, TypeVar, overload
from typing_extensions import assert_type

R = TypeVar("R")
T = TypeVar("T")

def infer_return(callback: Callable[[T], R]) -> R:
raise NotImplementedError

@overload
def callback(value: Literal["a"]): ...
@overload
def callback(value: Literal["b"]): ...
@overload
def callback(value: Literal["c"]): ...
@overload
def callback(value: Literal["d", "e"]): ...
@overload
def callback(value: Literal["f", "g"]): ...
@overload
def callback(value: Literal["h", "i"]): ...
@overload
def callback(value: Literal["j", "k"]): ...
@overload
def callback(value: object) -> object: ...
def callback(value):
raise NotImplementedError

assert_type(infer_return(callback), object)
```

## Generic inference after projection budget exhaustion

The literal-specific overloads below produce more alternative bindings than generic inference can
project within its limits. The precise type of `default=0` does not replace the missing callback
evidence: we recover with `Unknown` in either argument order.

```py
from typing import Callable, Literal, TypeVar, overload
from typing_extensions import assert_type
from ty_extensions._internal import Unknown

R = TypeVar("R")
T = TypeVar("T")

def infer_return(callback: Callable[[T], R], default: R) -> R:
raise NotImplementedError

@overload
def callback(value: Literal[0, 1]): ...
@overload
def callback(value: Literal[2, 3]): ...
@overload
def callback(value: Literal[4, 5]): ...
@overload
def callback(value: Literal[6, 7]): ...
@overload
def callback(value: Literal[8, 9]): ...
@overload
def callback(value: Literal[10, 11]): ...
@overload
def callback(value: Literal[12, 13]): ...
@overload
def callback(value: Literal[14, 15]): ...
@overload
def callback(value: Literal[16, 17]): ...
@overload
def callback(value: Literal[18, 19]): ...
@overload
def callback(value: Literal[20, 21]): ...
@overload
def callback(value: Literal[22, 23]): ...
@overload
def callback(value: Literal[24, 25]): ...
@overload
def callback(value: object) -> object: ...
def callback(value):
raise NotImplementedError

assert_type(infer_return(callback, 0), Unknown)
assert_type(infer_return(default=0, callback=callback), Unknown)
```

## Multiple occurrences of a higher-order generic callable

If a generic callable is used more than once in a higher-order call, each occurrence should get its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,42 @@ def _(values: list[Recursive], sink: Callable[[object], None]) -> None:
reveal_type(first_recursive(values, sink))
```

## Inferring from multiple intersection arguments

Each argument below satisfies `Source[T]` in two ways. Combining independent alternatives must
remain bounded, and the merged inference result retains evidence from all four arguments. Reordering
the arguments does not change that result.

```py
from typing import assert_type
from ty_extensions import Intersection

class Source[T]:
def get(self) -> T:
raise NotImplementedError

class A: ...
class B: ...
class C: ...
class D: ...
class E: ...
class F: ...
class G: ...
class H: ...

def first[T](a: Source[T], b: Source[T], c: Source[T], d: Source[T]) -> T:
return a.get()

def _(
a: Intersection[Source[A], Source[B]],
b: Intersection[Source[C], Source[D]],
c: Intersection[Source[E], Source[F]],
d: Intersection[Source[G], Source[H]],
) -> None:
assert_type(first(a, b, c, d), A | B | C | D | E | F | G | H)
assert_type(first(d, c, b, a), A | B | C | D | E | F | G | H)
```

## Typevars in a union

```py
Expand Down
20 changes: 11 additions & 9 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3060,8 +3060,8 @@ impl<'db> Bindings<'db> {

let constraints = ConstraintSetBuilder::new();
let set = constraints.load(db, env, tracked.constraints(db));
let result = match set.solutions(db, env, &constraints, inferable) {
Solutions::Constrained(paths) => Type::heterogeneous_tuple(
let result = match set.solutions(db, env, inferable) {
Ok(Solutions::Constrained(paths)) => Type::heterogeneous_tuple(
db,
env,
paths.into_vec().into_iter().map(|path| {
Expand All @@ -3074,8 +3074,9 @@ impl<'db> Bindings<'db> {
))
}),
),
Solutions::Unsatisfiable => Type::none(db, env),
Solutions::Unconstrained => Type::empty_tuple(db, env),
Ok(Solutions::Unsatisfiable) => Type::none(db, env),
Ok(Solutions::Unconstrained) => Type::empty_tuple(db, env),
Err(_) => Type::unknown(),
};
overload.set_return_type(result);
}
Expand All @@ -3097,8 +3098,8 @@ impl<'db> Bindings<'db> {

let constraints = ConstraintSetBuilder::new();
let set = constraints.load(db, env, tracked.constraints(db));
let result = match set.solutions(db, env, &constraints, inferable) {
Solutions::Constrained(paths) => Type::heterogeneous_tuple(
let result = match set.solutions(db, env, inferable) {
Ok(Solutions::Constrained(paths)) => Type::heterogeneous_tuple(
db,
env,
paths.into_vec().into_iter().map(|path| {
Expand All @@ -3110,8 +3111,9 @@ impl<'db> Bindings<'db> {
))
}),
),
Solutions::Unsatisfiable => Type::none(db, env),
Solutions::Unconstrained => Type::empty_tuple(db, env),
Ok(Solutions::Unsatisfiable) => Type::none(db, env),
Ok(Solutions::Unconstrained) => Type::empty_tuple(db, env),
Err(_) => Type::unknown(),
};
overload.set_return_type(result);
}
Expand Down Expand Up @@ -5859,7 +5861,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
self.inferable_typevars,
);

// Use `solutions_with` to determine per-typevar variance from the raw
// Use `solve_with` to determine per-typevar variance from the raw
// lower/upper bounds on each BDD path.
let mut variance_map: FxHashMap<BoundTypeVarIdentity<'_>, TypeVarVariance> =
FxHashMap::default();
Expand Down
Loading
Loading