Skip to content

Commit 8c21953

Browse files
authored
New type inference: add support for upper bounds and values (#15813)
This is a third PR in series following #15287 and #15754. This one is quite simple: I just add basic support for polymorphic inference involving type variables with upper bounds and values. A complete support would be quite complicated, and it will be a corner case to already rare situation. Finally, it is written in a way that is easy to tune in the future. I also use this PR to add some unit tests for all three PRs so far, other two PRs only added integration tests (and I clean up existing unit tests as well).
1 parent a7c4852 commit 8c21953

File tree

4 files changed

+277
-40
lines changed

4 files changed

+277
-40
lines changed

mypy/solve.py

+69-11
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
from mypy.expandtype import expand_type
1111
from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort
1212
from mypy.join import join_types
13-
from mypy.meet import meet_types
13+
from mypy.meet import meet_type_list, meet_types
1414
from mypy.subtypes import is_subtype
1515
from mypy.typeops import get_type_vars
1616
from mypy.types import (
1717
AnyType,
18+
Instance,
19+
NoneType,
1820
ProperType,
1921
Type,
2022
TypeOfAny,
@@ -108,15 +110,15 @@ def solve_constraints(
108110
else:
109111
candidate = AnyType(TypeOfAny.special_form)
110112
res.append(candidate)
111-
return res, [originals[tv] for tv in free_vars]
113+
return res, free_vars
112114

113115

114116
def solve_with_dependent(
115117
vars: list[TypeVarId],
116118
constraints: list[Constraint],
117119
original_vars: list[TypeVarId],
118120
originals: dict[TypeVarId, TypeVarLikeType],
119-
) -> tuple[Solutions, list[TypeVarId]]:
121+
) -> tuple[Solutions, list[TypeVarLikeType]]:
120122
"""Solve set of constraints that may depend on each other, like T <: List[S].
121123
122124
The whole algorithm consists of five steps:
@@ -135,23 +137,24 @@ def solve_with_dependent(
135137
raw_batches = list(topsort(prepare_sccs(sccs, dmap)))
136138

137139
free_vars = []
140+
free_solutions = {}
138141
for scc in raw_batches[0]:
139142
# If there are no bounds on this SCC, then the only meaningful solution we can
140143
# express, is that each variable is equal to a new free variable. For example,
141144
# if we have T <: S, S <: U, we deduce: T = S = U = <free>.
142145
if all(not lowers[tv] and not uppers[tv] for tv in scc):
143-
# For convenience with current type application machinery, we use a stable
144-
# choice that prefers the original type variables (not polymorphic ones) in SCC.
145-
# TODO: be careful about upper bounds (or values) when introducing free vars.
146-
free_vars.append(sorted(scc, key=lambda x: (x not in original_vars, x.raw_id))[0])
146+
best_free = choose_free([originals[tv] for tv in scc], original_vars)
147+
if best_free:
148+
free_vars.append(best_free.id)
149+
free_solutions[best_free.id] = best_free
147150

148151
# Update lowers/uppers with free vars, so these can now be used
149152
# as valid solutions.
150-
for l, u in graph.copy():
153+
for l, u in graph:
151154
if l in free_vars:
152-
lowers[u].add(originals[l])
155+
lowers[u].add(free_solutions[l])
153156
if u in free_vars:
154-
uppers[l].add(originals[u])
157+
uppers[l].add(free_solutions[u])
155158

156159
# Flatten the SCCs that are independent, we can solve them together,
157160
# since we don't need to update any targets in between.
@@ -166,7 +169,7 @@ def solve_with_dependent(
166169
for flat_batch in batches:
167170
res = solve_iteratively(flat_batch, graph, lowers, uppers)
168171
solutions.update(res)
169-
return solutions, free_vars
172+
return solutions, [free_solutions[tv] for tv in free_vars]
170173

171174

172175
def solve_iteratively(
@@ -276,6 +279,61 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None:
276279
return candidate
277280

278281

282+
def choose_free(
283+
scc: list[TypeVarLikeType], original_vars: list[TypeVarId]
284+
) -> TypeVarLikeType | None:
285+
"""Choose the best solution for an SCC containing only type variables.
286+
287+
This is needed to preserve e.g. the upper bound in a situation like this:
288+
def dec(f: Callable[[T], S]) -> Callable[[T], S]: ...
289+
290+
@dec
291+
def test(x: U) -> U: ...
292+
293+
where U <: A.
294+
"""
295+
296+
if len(scc) == 1:
297+
# Fast path, choice is trivial.
298+
return scc[0]
299+
300+
common_upper_bound = meet_type_list([t.upper_bound for t in scc])
301+
common_upper_bound_p = get_proper_type(common_upper_bound)
302+
# We include None for when strict-optional is disabled.
303+
if isinstance(common_upper_bound_p, (UninhabitedType, NoneType)):
304+
# This will cause to infer <nothing>, which is better than a free TypeVar
305+
# that has an upper bound <nothing>.
306+
return None
307+
308+
values: list[Type] = []
309+
for tv in scc:
310+
if isinstance(tv, TypeVarType) and tv.values:
311+
if values:
312+
# It is too tricky to support multiple TypeVars with values
313+
# within the same SCC.
314+
return None
315+
values = tv.values.copy()
316+
317+
if values and not is_trivial_bound(common_upper_bound_p):
318+
# If there are both values and upper bound present, we give up,
319+
# since type variables having both are not supported.
320+
return None
321+
322+
# For convenience with current type application machinery, we use a stable
323+
# choice that prefers the original type variables (not polymorphic ones) in SCC.
324+
best = sorted(scc, key=lambda x: (x.id not in original_vars, x.id.raw_id))[0]
325+
if isinstance(best, TypeVarType):
326+
return best.copy_modified(values=values, upper_bound=common_upper_bound)
327+
if is_trivial_bound(common_upper_bound_p):
328+
# TODO: support more cases for ParamSpecs/TypeVarTuples
329+
return best
330+
return None
331+
332+
333+
def is_trivial_bound(tp: ProperType) -> bool:
334+
return isinstance(tp, Instance) and tp.type.fullname == "builtins.object"
335+
336+
279337
def normalize_constraints(
280338
constraints: list[Constraint], vars: list[TypeVarId]
281339
) -> list[Constraint]:

0 commit comments

Comments
 (0)