Skip to content
Draft
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
5 changes: 3 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4366,8 +4366,9 @@ def check_lvalue(
types = [
self.check_lvalue(sub_expr)[0] or
# This type will be used as a context for further inference of rvalue,
# we put Uninhabited if there is no information available from lvalue.
UninhabitedType()
# we put AnyType if there is no information available from lvalue.
AnyType(TypeOfAny.unannotated)
# UninhabitedType() fails testInferenceNestedTuplesFromGenericIterable
for sub_expr in lvalue.items
]
lvalue_type = TupleType(types, self.named_type("builtins.tuple"))
Expand Down
704 changes: 584 additions & 120 deletions mypy/checkexpr.py

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@ class Constraint:
"""

type_var: TypeVarId
original_type_var: TypeVarLikeType
op = 0 # SUBTYPE_OF or SUPERTYPE_OF
target: Type

def __init__(self, type_var: TypeVarLikeType, op: int, target: Type) -> None:
self.type_var = type_var.id
self.original_type_var = type_var
self.op = op
# TODO: should we add "assert not isinstance(target, UnpackType)"?
# UnpackType is a synthetic type, and is never valid as a constraint target.
Expand Down Expand Up @@ -1356,7 +1358,11 @@ def visit_typeddict_type(self, template: TypedDictType) -> list[Constraint]:
# NOTE: Non-matching keys are ignored. Compatibility is checked
# elsewhere so this shouldn't be unsafe.
for item_name, template_item_type, actual_item_type in template.zip(actual):
res.extend(infer_constraints(template_item_type, actual_item_type, self.direction))
# Value type is invariant, so irrespective of the direction,
# we constrain both above and below.
# Fixes testTypedDictWideContext
res.extend(infer_constraints(template_item_type, actual_item_type, SUBTYPE_OF))
res.extend(infer_constraints(template_item_type, actual_item_type, SUPERTYPE_OF))
return res
elif isinstance(actual, AnyType):
return self.infer_against_any(template.items.values(), actual)
Expand Down
33 changes: 32 additions & 1 deletion mypy/erasetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,18 @@ def erase_meta_id(id: TypeVarId) -> bool:
return id.is_meta_var()


def replace_meta_vars(t: Type, target_type: Type) -> Type:
def replace_typevar(t: Type, tvar_id: TypeVarId, replacement: Type) -> Type:
"""Replace type variable in a type with the target type."""

def replace_id(id: TypeVarId) -> bool:
return id == tvar_id

return t.accept(TypeVarEraser(replace_id, replacement))


def replace_meta_vars(
t: Type, target_type: Type, ids_to_replace: Container[TypeVarId] | None = None
) -> Type:
"""Replace unification variables in a type with the target type."""
return t.accept(TypeVarEraser(erase_meta_id, target_type))

Expand Down Expand Up @@ -233,6 +244,26 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type:
return t.copy_modified(args=[a.accept(self) for a in t.args])


class TypeVarSubstitutor(TypeTranslator):
"""Substitute a type variable with a given replacement.

Args:
erase_id: A callable that returns True if the type variable should be replaced.
If None, all type variables are replaced.
replacement: The type to replace the type variable with.
covariant_replacement (optional): The type to replace the type variable when it is used in a covariant position.
contravariant_replacement (optional): The type to replace the type variable when it is used in a contravariant position.

Examples:
We have an upper bounded type variables `T <: MyType`
We know we have a type-var constraint `S <: T`
We can create a weaker constraint `S <: MyType` by using the upper bound
Likewise, when we have `S <: Callable[[T], R]`, we can create a weaker constraint `S <: Callable[[Never], R]`,
by using a lower bound of `T` which is `Never`.
So generally, we can use lower bounds for contravariant positions and upper bounds for covariant positions.
"""


def remove_instance_last_known_values(t: Type) -> Type:
return t.accept(LastKnownValueEraser())

Expand Down
10 changes: 10 additions & 0 deletions mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ def freshen_function_type_vars(callee: F) -> F:
return cast(F, fresh_overload)


def get_freshened_tvar_mapping(callee: CallableType) -> dict[TypeVarId, TypeVarLikeType]:
"""Substitute fresh type variables for generic function type variables."""
assert isinstance(callee, CallableType)
tvmap: dict[TypeVarId, TypeVarLikeType] = {}
for v in callee.variables:
tv = v.new_unification_variable(v)
tvmap[v.id] = tv
return tvmap


class HasGenericCallable(BoolTypeQuery):
def __init__(self) -> None:
super().__init__(ANY_STRATEGY)
Expand Down
8 changes: 7 additions & 1 deletion mypy/infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from mypy.constraints import (
SUBTYPE_OF,
SUPERTYPE_OF,
Constraint,
infer_constraints,
infer_constraints_for_callable,
)
Expand Down Expand Up @@ -39,6 +40,8 @@ def infer_function_type_arguments(
context: ArgumentInferContext,
strict: bool = True,
allow_polymorphic: bool = False,
extra_constraints: list[Constraint | None] = None,
minimize: bool = False,
) -> tuple[list[Type | None], list[TypeVarLikeType]]:
"""Infer the type arguments of a generic function.

Expand All @@ -58,9 +61,12 @@ def infer_function_type_arguments(
callee_type, arg_types, arg_kinds, arg_names, formal_to_actual, context
)

if extra_constraints:
constraints += extra_constraints

# Solve constraints.
type_vars = callee_type.variables
return solve_constraints(type_vars, constraints, strict, allow_polymorphic)
return solve_constraints(type_vars, constraints, strict, allow_polymorphic, minimize=minimize)


def infer_type_arguments(
Expand Down
1 change: 1 addition & 0 deletions mypy/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ def match_generic_callables(t: CallableType, s: CallableType) -> tuple[CallableT


def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
# join(X₁ -> Y₁, X₂ -> Y₂) = meet(X₁, X₂) -> join(Y₁, Y₂)
t, s = match_generic_callables(t, s)
arg_types: list[Type] = []
for i in range(len(t.arg_types)):
Expand Down
1 change: 1 addition & 0 deletions mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,7 @@ def default(self, typ: Type) -> ProperType:


def meet_similar_callables(t: CallableType, s: CallableType) -> CallableType:
# meet(X₁ -> Y₁, X₂ -> Y₂) = join(X₁, X₂) -> meet(Y₁, Y₂)
from mypy.join import match_generic_callables, safe_join

t, s = match_generic_callables(t, s)
Expand Down
63 changes: 47 additions & 16 deletions mypy/solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints, neg_op
from mypy.expandtype import expand_type
from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort
from mypy.join import join_type_list
from mypy.join import join_type_list, object_or_any_from_type
from mypy.meet import meet_type_list, meet_types
from mypy.subtypes import is_subtype
from mypy.typeops import get_all_type_vars
Expand Down Expand Up @@ -44,6 +44,8 @@ def solve_constraints(
strict: bool = True,
allow_polymorphic: bool = False,
skip_unsatisfied: bool = False,
minimize: bool = False,
maximize: bool = False,
) -> tuple[list[Type | None], list[TypeVarLikeType]]:
"""Solve type constraints.

Expand Down Expand Up @@ -82,7 +84,7 @@ def solve_constraints(
if allow_polymorphic:
if constraints:
solutions, free_vars = solve_with_dependent(
vars + extra_vars, constraints, vars, originals
vars + extra_vars, constraints, vars, originals, minimize=minimize
)
else:
solutions = {}
Expand All @@ -95,7 +97,7 @@ def solve_constraints(
continue
lowers = [c.target for c in cs if c.op == SUPERTYPE_OF]
uppers = [c.target for c in cs if c.op == SUBTYPE_OF]
solution = solve_one(lowers, uppers)
solution = solve_one(lowers, uppers, minimize=minimize, maximize=maximize)

# Do not leak type variables in non-polymorphic solutions.
if solution is None or not get_vars(
Expand Down Expand Up @@ -131,6 +133,7 @@ def solve_with_dependent(
constraints: list[Constraint],
original_vars: list[TypeVarId],
originals: dict[TypeVarId, TypeVarLikeType],
minimize: bool = False,
) -> tuple[Solutions, list[TypeVarLikeType]]:
"""Solve set of constraints that may depend on each other, like T <: List[S].

Expand Down Expand Up @@ -182,13 +185,13 @@ def solve_with_dependent(

solutions: dict[TypeVarId, Type | None] = {}
for flat_batch in batches:
res = solve_iteratively(flat_batch, graph, lowers, uppers)
res = solve_iteratively(flat_batch, graph, lowers, uppers, minimize=minimize)
solutions.update(res)
return solutions, [free_solutions[tv] for tv in free_vars]


def solve_iteratively(
batch: list[TypeVarId], graph: Graph, lowers: Bounds, uppers: Bounds
batch: list[TypeVarId], graph: Graph, lowers: Bounds, uppers: Bounds, minimize: bool = False
) -> Solutions:
"""Solve transitive closure sequentially, updating upper/lower bounds after each step.

Expand All @@ -214,7 +217,7 @@ def solve_iteratively(
break
# Solve each solvable type variable separately.
s_batch.remove(solvable_tv)
result = solve_one(lowers[solvable_tv], uppers[solvable_tv])
result = solve_one(lowers[solvable_tv], uppers[solvable_tv], minimize=minimize)
solutions[solvable_tv] = result
if result is None:
# TODO: support backtracking lower/upper bound choices and order within SCCs.
Expand Down Expand Up @@ -256,7 +259,9 @@ def _join_sorted_key(t: Type) -> int:
return 0


def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None:
def solve_one(
lowers: Iterable[Type], uppers: Iterable[Type], minimize: bool = False, maximize: bool = False
) -> Type | None:
"""Solve constraints by finding by using meets of upper bounds, and joins of lower bounds."""

candidate: Type | None = None
Expand Down Expand Up @@ -310,18 +315,44 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None:
source_any = top if isinstance(p_top, AnyType) else bottom
assert isinstance(source_any, ProperType) and isinstance(source_any, AnyType)
return AnyType(TypeOfAny.from_another_any, source_any=source_any)
elif bottom is None:
if top:

assert not (minimize and maximize)

if minimize: # pick minimum solution
if bottom is None and top is None:
return None
elif bottom is None:
candidate = UninhabitedType()
elif top is None:
candidate = bottom
elif is_subtype(bottom, top):
candidate = bottom
else:
candidate = None
elif maximize: # choose "largest" solution
if bottom is None and top is None:
return None
elif bottom is None:
candidate = top
elif top is None:
assert p_bottom is not None
candidate = object_or_any_from_type(p_bottom)
elif is_subtype(bottom, top):
candidate = top
else:
# No constraints for type variable
candidate = None
else: # choose "best" solution
if bottom is None and top is None:
return None
elif top is None:
candidate = bottom
elif is_subtype(bottom, top):
candidate = bottom
else:
candidate = None
elif bottom is None:
candidate = top
elif top is None:
candidate = bottom
elif is_subtype(bottom, top):
candidate = bottom
else:
candidate = None

return candidate


Expand Down
37 changes: 37 additions & 0 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
)
from mypy.options import Options
from mypy.state import state
from mypy.typeops import get_all_type_vars
from mypy.types import (
MYPYC_NATIVE_INT_NAMES,
TUPLE_LIKE_INSTANCE_NAMES,
Expand All @@ -61,6 +62,8 @@
TypedDictType,
TypeOfAny,
TypeType,
TypeVarId,
TypeVarLikeType,
TypeVarTupleType,
TypeVarType,
TypeVisitor,
Expand Down Expand Up @@ -2175,6 +2178,40 @@ def all_non_object_members(info: TypeInfo) -> set[str]:
return members


def infer_variance_in_expr(type_form: Type, tvar: TypeVarLikeType) -> int:
r"""Infer the variance of the ith type variable in a type expression.

Assume we have a type expression `TypeForm[T1, ..., Tn]` with type variables T1, ..., Tn.

Then this method returns:

1. COVARIANT, if X <: T1 implies TypeForm[X, T2, ..., Tn] <: TypeForm[T1, T2, ..., Tn]
2. CONTRAVARIANT, if X <: T1 implies TypeForm[X, T2, ..., Tn] :> TypeForm[T1, T2, ..., Tn]
3. INVARIANT, if neither of the above holds
"""
# 0. If the type variable does not appear in the type expression, return INVARIANT.
if tvar not in get_all_type_vars(type_form):
return INVARIANT

fresh_var = TypeVarType(
"X",
"X",
id=TypeVarId(-2),
values=[],
# Use other TypeVar as the upper bound
# This is not officially supported, but does seem to work?
upper_bound=tvar,
default=AnyType(TypeOfAny.from_omitted_generics),
)
new_form = expand_type(type_form, {tvar.id: fresh_var})

if is_subtype(new_form, type_form):
return COVARIANT
if is_subtype(type_form, new_form):
return CONTRAVARIANT
return INVARIANT


def infer_variance(info: TypeInfo, i: int) -> bool:
"""Infer the variance of the ith type variable of a generic class.

Expand Down
6 changes: 5 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3029,7 +3029,11 @@ def copy_modified(
if item_names is not None:
items = {k: v for (k, v) in items.items() if k in item_names}
required_keys &= set(item_names)
return TypedDictType(items, required_keys, readonly_keys, fallback, self.line, self.column)
result = TypedDictType(
items, required_keys, readonly_keys, fallback, self.line, self.column
)
result.to_be_mutated = self.to_be_mutated
return result

def create_anonymous_fallback(self) -> Instance:
anonymous = self.as_anonymous()
Expand Down
4 changes: 3 additions & 1 deletion mypyc/irbuild/specialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,9 @@ def get_literal_str(expr: Expression) -> str | None:
if isinstance(expr, StrExpr):
return expr.value
elif isinstance(expr, RefExpr) and isinstance(expr.node, Var) and expr.node.is_final:
return str(expr.node.final_value)
final_value = expr.node.final_value
if final_value is not None:
return str(final_value)
return None

for i in range(len(exprs) - 1):
Expand Down
9 changes: 9 additions & 0 deletions mypyc/test-data/run-strings.test
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,16 @@ def test_basics() -> None:
[case testFStrings]
import decimal
from datetime import datetime
from typing import Final

var = 'mypyc'
num = 20
final_known_at_compile_time: Final = 'hello'

def final_value_setter() -> str:
return 'goodbye'

final_unknown_at_compile_time: Final = final_value_setter()

def test_fstring_basics() -> None:
assert f'Hello {var}, this is a test' == "Hello mypyc, this is a test"
Expand Down Expand Up @@ -451,6 +458,8 @@ def test_fstring_basics() -> None:
inf_num = float('inf')
assert f'{nan_num}, {inf_num}' == 'nan, inf'

assert f'{final_known_at_compile_time} {final_unknown_at_compile_time}' == 'hello goodbye'

# F-strings would be translated into ''.join[string literals, format method call, ...] in mypy AST.
# Currently we are using a str.join specializer for f-string speed up. We might not cover all cases
# and the rest ones should fall back to a normal str.join method call.
Expand Down
Loading
Loading