Skip to content

Commit b50f3a1

Browse files
authored
Use union types instead of join in binder (#18538)
This would be more consistent with what we already do for ternary expressions. Note the change in match test results from match logic not handling well the situation when initial type is a union. A possible workaround would be to force "collapsing" union of tuples back into a tuple with union, but it is not easy and was planning to do some cleanup in the match handling as well (in particular it uses joins instead of unions in a way that will be inconsistent with new binder behavior). I want to put the switch from join to union for match statement in a separate PR. Note I also simplify a bunch of special-casing around `Any` in the binder that existed mostly because `join(Any, X) == Any`. Fixes #3724
1 parent 8dd616b commit b50f3a1

17 files changed

+138
-152
lines changed

mypy/binder.py

Lines changed: 52 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from typing_extensions import TypeAlias as _TypeAlias
88

99
from mypy.erasetype import remove_instance_last_known_values
10-
from mypy.join import join_simple
1110
from mypy.literals import Key, literal, literal_hash, subkeys
1211
from mypy.nodes import Expression, IndexExpr, MemberExpr, NameExpr, RefExpr, TypeInfo, Var
1312
from mypy.subtypes import is_same_type, is_subtype
13+
from mypy.typeops import make_simplified_union
1414
from mypy.types import (
1515
AnyType,
1616
Instance,
@@ -21,6 +21,7 @@
2121
Type,
2222
TypeOfAny,
2323
TypeType,
24+
TypeVarType,
2425
UnionType,
2526
UnpackType,
2627
find_unpack_in_list,
@@ -237,9 +238,21 @@ def update_from_options(self, frames: list[Frame]) -> bool:
237238
):
238239
type = AnyType(TypeOfAny.from_another_any, source_any=declaration_type)
239240
else:
240-
for other in resulting_values[1:]:
241-
assert other is not None
242-
type = join_simple(self.declarations[key], type, other.type)
241+
possible_types = []
242+
for t in resulting_values:
243+
assert t is not None
244+
possible_types.append(t.type)
245+
if len(possible_types) == 1:
246+
# This is to avoid calling get_proper_type() unless needed, as this may
247+
# interfere with our (hacky) TypeGuard support.
248+
type = possible_types[0]
249+
else:
250+
type = make_simplified_union(possible_types)
251+
# Legacy guard for corner case when the original type is TypeVarType.
252+
if isinstance(declaration_type, TypeVarType) and not is_subtype(
253+
type, declaration_type
254+
):
255+
type = declaration_type
243256
# Try simplifying resulting type for unions involving variadic tuples.
244257
# Technically, everything is still valid without this step, but if we do
245258
# not do this, this may create long unions after exiting an if check like:
@@ -258,7 +271,7 @@ def update_from_options(self, frames: list[Frame]) -> bool:
258271
)
259272
if simplified == self.declarations[key]:
260273
type = simplified
261-
if current_value is None or not is_same_type(type, current_value[0]):
274+
if current_value is None or not is_same_type(type, current_value.type):
262275
self._put(key, type, from_assignment=True)
263276
changed = True
264277

@@ -300,9 +313,7 @@ def accumulate_type_assignments(self) -> Iterator[Assigns]:
300313
yield self.type_assignments
301314
self.type_assignments = old_assignments
302315

303-
def assign_type(
304-
self, expr: Expression, type: Type, declared_type: Type | None, restrict_any: bool = False
305-
) -> None:
316+
def assign_type(self, expr: Expression, type: Type, declared_type: Type | None) -> None:
306317
# We should erase last known value in binder, because if we are using it,
307318
# it means that the target is not final, and therefore can't hold a literal.
308319
type = remove_instance_last_known_values(type)
@@ -333,41 +344,39 @@ def assign_type(
333344

334345
p_declared = get_proper_type(declared_type)
335346
p_type = get_proper_type(type)
336-
enclosing_type = get_proper_type(self.most_recent_enclosing_type(expr, type))
337-
if isinstance(enclosing_type, AnyType) and not restrict_any:
338-
# If x is Any and y is int, after x = y we do not infer that x is int.
339-
# This could be changed.
340-
# Instead, since we narrowed type from Any in a recent frame (probably an
341-
# isinstance check), but now it is reassigned, we broaden back
342-
# to Any (which is the most recent enclosing type)
343-
self.put(expr, enclosing_type)
344-
# As a special case, when assigning Any to a variable with a
345-
# declared Optional type that has been narrowed to None,
346-
# replace all the Nones in the declared Union type with Any.
347-
# This overrides the normal behavior of ignoring Any assignments to variables
348-
# in order to prevent false positives.
349-
# (See discussion in #3526)
350-
elif (
351-
isinstance(p_type, AnyType)
352-
and isinstance(p_declared, UnionType)
353-
and any(isinstance(get_proper_type(item), NoneType) for item in p_declared.items)
354-
and isinstance(
355-
get_proper_type(self.most_recent_enclosing_type(expr, NoneType())), NoneType
356-
)
357-
):
358-
# Replace any Nones in the union type with Any
359-
new_items = [
360-
type if isinstance(get_proper_type(item), NoneType) else item
361-
for item in p_declared.items
362-
]
363-
self.put(expr, UnionType(new_items))
364-
elif isinstance(p_type, AnyType) and not (
365-
isinstance(p_declared, UnionType)
366-
and any(isinstance(get_proper_type(item), AnyType) for item in p_declared.items)
367-
):
368-
# Assigning an Any value doesn't affect the type to avoid false negatives, unless
369-
# there is an Any item in a declared union type.
370-
self.put(expr, declared_type)
347+
if isinstance(p_type, AnyType):
348+
# Any type requires some special casing, for both historical reasons,
349+
# and to optimise user experience without sacrificing correctness too much.
350+
if isinstance(expr, RefExpr) and isinstance(expr.node, Var) and expr.node.is_inferred:
351+
# First case: a local/global variable without explicit annotation,
352+
# in this case we just assign Any (essentially following the SSA logic).
353+
self.put(expr, type)
354+
elif isinstance(p_declared, UnionType) and any(
355+
isinstance(get_proper_type(item), NoneType) for item in p_declared.items
356+
):
357+
# Second case: explicit optional type, in this case we optimize for a common
358+
# pattern when an untyped value used as a fallback replacing None.
359+
new_items = [
360+
type if isinstance(get_proper_type(item), NoneType) else item
361+
for item in p_declared.items
362+
]
363+
self.put(expr, UnionType(new_items))
364+
elif isinstance(p_declared, UnionType) and any(
365+
isinstance(get_proper_type(item), AnyType) for item in p_declared.items
366+
):
367+
# Third case: a union already containing Any (most likely from an un-imported
368+
# name), in this case we allow assigning Any as well.
369+
self.put(expr, type)
370+
else:
371+
# In all other cases we don't narrow to Any to minimize false negatives.
372+
self.put(expr, declared_type)
373+
elif isinstance(p_declared, AnyType):
374+
# Mirroring the first case above, we don't narrow to a precise type if the variable
375+
# has an explicit `Any` type annotation.
376+
if isinstance(expr, RefExpr) and isinstance(expr.node, Var) and expr.node.is_inferred:
377+
self.put(expr, type)
378+
else:
379+
self.put(expr, declared_type)
371380
else:
372381
self.put(expr, type)
373382

@@ -389,19 +398,6 @@ def invalidate_dependencies(self, expr: BindableExpression) -> None:
389398
for dep in self.dependencies.get(key, set()):
390399
self._cleanse_key(dep)
391400

392-
def most_recent_enclosing_type(self, expr: BindableExpression, type: Type) -> Type | None:
393-
type = get_proper_type(type)
394-
if isinstance(type, AnyType):
395-
return get_declaration(expr)
396-
key = literal_hash(expr)
397-
assert key is not None
398-
enclosers = [get_declaration(expr)] + [
399-
f.types[key].type
400-
for f in self.frames
401-
if key in f.types and is_subtype(type, f.types[key][0])
402-
]
403-
return enclosers[-1]
404-
405401
def allow_jump(self, index: int) -> None:
406402
# self.frames and self.options_on_return have different lengths
407403
# so make sure the index is positive

mypy/checker.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3284,7 +3284,7 @@ def check_assignment(
32843284
if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType):
32853285
# Don't use type binder for definitions of special forms, like named tuples.
32863286
if not (isinstance(lvalue, NameExpr) and lvalue.is_special_form):
3287-
self.binder.assign_type(lvalue, rvalue_type, lvalue_type, False)
3287+
self.binder.assign_type(lvalue, rvalue_type, lvalue_type)
32883288
if (
32893289
isinstance(lvalue, NameExpr)
32903290
and isinstance(lvalue.node, Var)
@@ -4023,7 +4023,7 @@ def check_multi_assignment_from_union(
40234023
if isinstance(expr, StarExpr):
40244024
expr = expr.expr
40254025

4026-
# TODO: See todo in binder.py, ConditionalTypeBinder.assign_type
4026+
# TODO: See comment in binder.py, ConditionalTypeBinder.assign_type
40274027
# It's unclear why the 'declared_type' param is sometimes 'None'
40284028
clean_items: list[tuple[Type, Type]] = []
40294029
for type, declared_type in items:
@@ -4035,7 +4035,6 @@ def check_multi_assignment_from_union(
40354035
expr,
40364036
make_simplified_union(list(types)),
40374037
make_simplified_union(list(declared_types)),
4038-
False,
40394038
)
40404039
for union, lv in zip(union_types, self.flatten_lvalues(lvalues)):
40414040
# Properly store the inferred types.
@@ -5233,7 +5232,7 @@ def visit_del_stmt(self, s: DelStmt) -> None:
52335232
for elt in flatten(s.expr):
52345233
if isinstance(elt, NameExpr):
52355234
self.binder.assign_type(
5236-
elt, DeletedType(source=elt.name), get_declaration(elt), False
5235+
elt, DeletedType(source=elt.name), get_declaration(elt)
52375236
)
52385237

52395238
def visit_decorator(self, e: Decorator) -> None:

mypy/fastparse.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1106,7 +1106,9 @@ def make_argument(
11061106
if argument_elide_name(arg.arg):
11071107
pos_only = True
11081108

1109-
argument = Argument(Var(arg.arg, arg_type), arg_type, self.visit(default), kind, pos_only)
1109+
var = Var(arg.arg, arg_type)
1110+
var.is_inferred = False
1111+
argument = Argument(var, arg_type, self.visit(default), kind, pos_only)
11101112
argument.set_line(
11111113
arg.lineno,
11121114
arg.col_offset,

mypy/join.py

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -183,55 +183,6 @@ def join_instances_via_supertype(self, t: Instance, s: Instance) -> ProperType:
183183
return best
184184

185185

186-
def join_simple(declaration: Type | None, s: Type, t: Type) -> ProperType:
187-
"""Return a simple least upper bound given the declared type.
188-
189-
This function should be only used by binder, and should not recurse.
190-
For all other uses, use `join_types()`.
191-
"""
192-
declaration = get_proper_type(declaration)
193-
s = get_proper_type(s)
194-
t = get_proper_type(t)
195-
196-
if (s.can_be_true, s.can_be_false) != (t.can_be_true, t.can_be_false):
197-
# if types are restricted in different ways, use the more general versions
198-
s = mypy.typeops.true_or_false(s)
199-
t = mypy.typeops.true_or_false(t)
200-
201-
if isinstance(s, AnyType):
202-
return s
203-
204-
if isinstance(s, ErasedType):
205-
return t
206-
207-
if is_proper_subtype(s, t, ignore_promotions=True):
208-
return t
209-
210-
if is_proper_subtype(t, s, ignore_promotions=True):
211-
return s
212-
213-
if isinstance(declaration, UnionType):
214-
return mypy.typeops.make_simplified_union([s, t])
215-
216-
if isinstance(s, NoneType) and not isinstance(t, NoneType):
217-
s, t = t, s
218-
219-
if isinstance(s, UninhabitedType) and not isinstance(t, UninhabitedType):
220-
s, t = t, s
221-
222-
# Meets/joins require callable type normalization.
223-
s, t = normalize_callables(s, t)
224-
225-
if isinstance(s, UnionType) and not isinstance(t, UnionType):
226-
s, t = t, s
227-
228-
value = t.accept(TypeJoinVisitor(s))
229-
if declaration is None or is_subtype(value, declaration):
230-
return value
231-
232-
return declaration
233-
234-
235186
def trivial_join(s: Type, t: Type) -> Type:
236187
"""Return one of types (expanded) if it is a supertype of other, otherwise top type."""
237188
if is_subtype(s, t):

mypy/test/testtypes.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from mypy.erasetype import erase_type, remove_instance_last_known_values
99
from mypy.indirection import TypeIndirectionVisitor
10-
from mypy.join import join_simple, join_types
10+
from mypy.join import join_types
1111
from mypy.meet import meet_types, narrow_declared_type
1212
from mypy.nodes import (
1313
ARG_NAMED,
@@ -817,12 +817,12 @@ def test_any_type(self) -> None:
817817
self.assert_join(t, self.fx.anyt, self.fx.anyt)
818818

819819
def test_mixed_truth_restricted_type_simple(self) -> None:
820-
# join_simple against differently restricted truthiness types drops restrictions.
820+
# make_simplified_union against differently restricted truthiness types drops restrictions.
821821
true_a = true_only(self.fx.a)
822822
false_o = false_only(self.fx.o)
823-
j = join_simple(self.fx.o, true_a, false_o)
824-
assert j.can_be_true
825-
assert j.can_be_false
823+
u = make_simplified_union([true_a, false_o])
824+
assert u.can_be_true
825+
assert u.can_be_false
826826

827827
def test_mixed_truth_restricted_type(self) -> None:
828828
# join_types against differently restricted truthiness types drops restrictions.

mypyc/test-data/irbuild-any.test

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ def f(a: Any, n: int, c: C) -> None:
3737
c.n = a
3838
a = n
3939
n = a
40-
a.a = n
4140
[out]
4241
def f(a, n, c):
4342
a :: object
@@ -49,10 +48,6 @@ def f(a, n, c):
4948
r3 :: bool
5049
r4 :: object
5150
r5 :: int
52-
r6 :: str
53-
r7 :: object
54-
r8 :: i32
55-
r9 :: bit
5651
L0:
5752
r0 = box(int, n)
5853
c.a = r0; r1 = is_error
@@ -62,10 +57,6 @@ L0:
6257
a = r4
6358
r5 = unbox(int, a)
6459
n = r5
65-
r6 = 'a'
66-
r7 = box(int, n)
67-
r8 = PyObject_SetAttr(a, r6, r7)
68-
r9 = r8 >= 0 :: signed
6960
return 1
7061

7162
[case testCoerceAnyInOps]

test-data/unit/check-classes.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3709,7 +3709,7 @@ def new(uc: Type[U]) -> U:
37093709
if 1:
37103710
u = uc(0)
37113711
u.foo()
3712-
u = uc('') # Error
3712+
uc('') # Error
37133713
u.foo(0) # Error
37143714
return uc()
37153715
u = new(User)

test-data/unit/check-dynamic-typing.test

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ if int():
252252
if int():
253253
a = d.foo(a, a)
254254
d.x = a
255-
d.x.y.z # E: "A" has no attribute "y"
255+
d.x.y.z
256256

257257
class A: pass
258258
[out]
@@ -320,8 +320,10 @@ d = None # All ok
320320
d = t
321321
d = g
322322
d = A
323-
t = d
324-
f = d
323+
324+
d1: Any
325+
t = d1
326+
f = d1
325327
[builtins fixtures/tuple.pyi]
326328

327329

test-data/unit/check-isinstance.test

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ if int():
115115
x = B()
116116
x.z
117117
x = foo()
118-
x.z # E: "A" has no attribute "z"
119-
x.y
118+
reveal_type(x) # N: Revealed type is "Any"
119+
reveal_type(x) # N: Revealed type is "__main__.A"
120120

121121
[case testSingleMultiAssignment]
122122
x = 'a'
@@ -1915,17 +1915,28 @@ if isinstance(x, str, 1): # E: Too many arguments for "isinstance"
19151915
reveal_type(x) # N: Revealed type is "builtins.int"
19161916
[builtins fixtures/isinstancelist.pyi]
19171917

1918-
[case testIsinstanceNarrowAny]
1918+
[case testIsinstanceNarrowAnyExplicit]
19191919
from typing import Any
19201920

19211921
def narrow_any_to_str_then_reassign_to_int() -> None:
1922-
v = 1 # type: Any
1922+
v: Any = 1
19231923

19241924
if isinstance(v, str):
19251925
reveal_type(v) # N: Revealed type is "builtins.str"
19261926
v = 2
19271927
reveal_type(v) # N: Revealed type is "Any"
1928+
[builtins fixtures/isinstance.pyi]
19281929

1930+
[case testIsinstanceNarrowAnyImplicit]
1931+
def foo(): ...
1932+
1933+
def narrow_any_to_str_then_reassign_to_int() -> None:
1934+
v = foo()
1935+
1936+
if isinstance(v, str):
1937+
reveal_type(v) # N: Revealed type is "builtins.str"
1938+
v = 2
1939+
reveal_type(v) # N: Revealed type is "builtins.int"
19291940
[builtins fixtures/isinstance.pyi]
19301941

19311942
[case testNarrowTypeAfterInList]

0 commit comments

Comments
 (0)