Skip to content

Commit 810cab3

Browse files
authored
[ty] Use existential quantification to only consider inferable typevars (astral-sh#24383)
The `ConstraintSet::solutions` method returns a (set of) solutions for a constraint set — assignments of specific types to each typevar in question. astral-sh#23848 introduced two variants of this method. One of them (`solutions_with_inferable`) would take in the set of `inferable` typevars. This was used in a cycle check at the beginning of the method, to make sure that we only considered the typevars we're actually solving for when detecting a cyclic constraint set. More importantly, it was also used to limit the result, so that we would only get solutions for ther inferable typevars (i.e., the ones that we're using the constraint set to solve for). A cleaner approach is to use _extensional quantification_ to _remove_ the non-inferable typevars from the constraint set before calculating solutions. We already had this available as a lower-level TDD method (`abstract_one_inner`), which lets us provide an arbitrary `should_remove` callback to determine which constraints to keep and which to remove. We just need to add a new public API method that provides a `should_remove` callback that keeps only the constraints involving inferable typevars. Given this change we can actually remove the cyclic checks completely, since `SequentMap` and `PathAssignments` will already bottom out if they encounter a cycle in the constraints. (Specifically, while we're walking TDD paths, `PathAssignments` will only add constraints that aren't already present in the current path.)
1 parent 4add3b1 commit 810cab3

5 files changed

Lines changed: 141 additions & 187 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Regressions for correlated constraints
2+
3+
This test exercises several regressions that stem from how our specialization inference does not
4+
always currently combine multiple constraints that we infer when calling a generic function.
5+
6+
## Generic protocol overloads
7+
8+
The generic protocol overload for `Series.mul` can infer multiple correlated specializations for
9+
`(T_contra, S2)`.
10+
11+
TODO: We currently collapse those disjunctive solutions into independent unions in
12+
`SpecializationBuilder.types`, which can produce an impossible pair and reject the overload. This
13+
should be fixed once we are using a constraint set for our internal state.
14+
15+
```toml
16+
[environment]
17+
python-version = "3.13"
18+
```
19+
20+
```py
21+
from typing import Generic, Protocol, TypeVar, overload
22+
23+
T = TypeVar("T")
24+
T_contra = TypeVar("T_contra")
25+
S2 = TypeVar("S2")
26+
27+
class ElementOpsMixin(Generic[S2]):
28+
@overload
29+
def _proto_mul(self: "ElementOpsMixin[bool]", other: bool) -> "ElementOpsMixin[bool]": ...
30+
@overload
31+
def _proto_mul(self: "ElementOpsMixin[str]", other: str) -> "ElementOpsMixin[str]": ...
32+
def _proto_mul(self, other):
33+
raise NotImplementedError
34+
35+
class Supports_ProtoMul(Protocol[T_contra, S2]):
36+
def _proto_mul(self, other: T_contra, /) -> ElementOpsMixin[S2]: ...
37+
38+
class Series(ElementOpsMixin[T], Generic[T]):
39+
@overload
40+
def mul(self: Supports_ProtoMul[T_contra, S2], other: T_contra) -> "Series[S2]": ...
41+
@overload
42+
def mul(self: "Series[int]", other: int) -> "Series[int]": ...
43+
def mul(self, other):
44+
raise NotImplementedError
45+
46+
def _(left: Series[bool]):
47+
# TODO: no error
48+
# TODO: revealed: Series[bool]
49+
# error: [no-matching-overload]
50+
# revealed: Unknown
51+
reveal_type(left.mul(True))
52+
```

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4340,29 +4340,23 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
43404340
let return_ty =
43414341
return_ty.filter_disjoint_elements(self.db, tcx, self.inferable_typevars);
43424342
let tcx = tcx.filter_disjoint_elements(self.db, return_ty, self.inferable_typevars);
4343-
let set = return_ty.when_constraint_set_assignable_to(self.db, tcx, constraints);
4343+
let set = return_ty
4344+
.when_constraint_set_assignable_to(self.db, tcx, constraints)
4345+
.remove_noninferable(self.db, constraints, self.inferable_typevars);
43444346

43454347
// Use `solutions_with` to determine per-typevar variance from the raw
43464348
// lower/upper bounds on each BDD path.
43474349
let mut variance_map: FxHashMap<BoundTypeVarIdentity<'_>, TypeVarVariance> =
43484350
FxHashMap::default();
4349-
let solutions = set.solutions_with_inferable(
4350-
self.db,
4351-
constraints,
4352-
self.inferable_typevars,
4353-
|typevar, variance, lower, upper| {
4354-
if !typevar.is_inferable(self.db, self.inferable_typevars) {
4355-
return Ok(None);
4356-
}
4357-
4351+
let solutions =
4352+
set.solutions_with(self.db, constraints, |typevar, variance, lower, upper| {
43584353
let identity = typevar.identity(self.db);
43594354
variance_map
43604355
.entry(identity)
43614356
.and_modify(|current| *current = current.join(variance))
43624357
.or_insert(variance);
4363-
PathBounds::default_solve(self.db, typevar, lower, upper)
4364-
},
4365-
);
4358+
PathBounds::default_solve(self.db, constraints, typevar, lower, upper)
4359+
});
43664360

43674361
let Solutions::Constrained(solutions) = solutions else {
43684362
return None;

0 commit comments

Comments
 (0)