Repository: quora/pyanalyze
Pull request #453 (2022-01-31 00:48:35)

Improve `TypeVar` solution algorithm


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 42aa2fa..f453a0f 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Improve `TypeVar` solution algorithm (#453)
 - Cache decisions about whether classes implement protocols (#450)
 - Fix application of multiple suggested changes per file
   when an earlier change has added or removed lines (#449)
diff --git a/pyanalyze/__init__.py b/pyanalyze/__init__.py
index 0308a96..3c40cb5 100644
--- a/pyanalyze/__init__.py
+++ b/pyanalyze/__init__.py
@@ -32,6 +32,7 @@ from . import stacked_scopes
 from . import suggested_type
 from . import type_object
 from . import typeshed
+from . import typevar
 from . import tests
 from . import value
 from .value import assert_is_value as assert_is_value, dump_value as dump_value
@@ -59,3 +60,4 @@ used(options)
 used(shared_options)
 used(functions)
 used(predicates)
+used(typevar)
diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py
index afaeb27..7e77cb0 100644
--- a/pyanalyze/annotations.py
+++ b/pyanalyze/annotations.py
@@ -444,17 +444,7 @@ def _type_from_runtime(val: Any, ctx: Context, is_typeddict: bool = False) -> Va
             return AnyValue(AnySource.error)
     elif typing_inspect.is_typevar(val):
         tv = cast(TypeVar, val)
-        if tv.__bound__ is not None:
-            bound = _type_from_runtime(tv.__bound__, ctx)
-        else:
-            bound = None
-        if tv.__constraints__:
-            constraints = tuple(
-                _type_from_runtime(constraint, ctx) for constraint in tv.__constraints__
-            )
-        else:
-            constraints = ()
-        return TypeVarValue(tv, bound=bound, constraints=constraints)
+        return make_type_var_value(tv, ctx)
     elif is_instance_of_typing_name(val, "ParamSpec"):
         return TypeVarValue(val, is_paramspec=True)
     elif is_instance_of_typing_name(val, "ParamSpecArgs"):
@@ -539,6 +529,20 @@ def _type_from_runtime(val: Any, ctx: Context, is_typeddict: bool = False) -> Va
         return AnyValue(AnySource.error)
 
 
+def make_type_var_value(tv: TypeVar, ctx: Context) -> TypeVarValue:
+    if tv.__bound__ is not None:
+        bound = _type_from_runtime(tv.__bound__, ctx)
+    else:
+        bound = None
+    if tv.__constraints__:
+        constraints = tuple(
+            _type_from_runtime(constraint, ctx) for constraint in tv.__constraints__
+        )
+    else:
+        constraints = ()
+    return TypeVarValue(tv, bound=bound, constraints=constraints)
+
+
 def _callable_args_from_runtime(
     arg_types: Any, label: str, ctx: Context
 ) -> Sequence[SigParameter]:
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index a2ca344..f1c162c 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -158,6 +158,7 @@ from .value import (
     NoReturnConstraintExtension,
     annotate_value,
     flatten_values,
+    get_tv_map,
     is_union,
     kv_pairs_from_mapping,
     make_weak,
@@ -2973,7 +2974,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
         return return_value
 
     def unpack_awaitable(self, composite: Composite, node: ast.AST) -> Value:
-        tv_map = AwaitableValue.can_assign(composite.value, self)
+        tv_map = get_tv_map(AwaitableValue, composite.value, self)
         if isinstance(tv_map, CanAssignError):
             return self._check_dunder_call(node, composite, "__await__", [])
         else:
@@ -3106,14 +3107,14 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
             not (self.is_generator and self.async_kind == AsyncFunctionKind.non_async)
             and self.expected_return_value is not None
         ):
-            tv_map = self.expected_return_value.can_assign(value, self)
-            if isinstance(tv_map, CanAssignError):
+            can_assign = self.expected_return_value.can_assign(value, self)
+            if isinstance(can_assign, CanAssignError):
                 self._show_error_if_checking(
                     node,
                     f"Declared return type {self.expected_return_value} is incompatible"
                     f" with actual return type {value}",
                     error_code=ErrorCode.incompatible_return_value,
-                    detail=tv_map.display(),
+                    detail=can_assign.display(),
                 )
         if (
             self.expected_return_value == KnownNone
@@ -3340,7 +3341,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
         else:
             protocol = "typing.ContextManager"
         val = GenericValue(protocol, [TypeVarValue(T)])
-        can_assign = val.can_assign(context, self)
+        can_assign = get_tv_map(val, context, self)
         if isinstance(can_assign, CanAssignError):
             self._show_error_if_checking(
                 node.context_expr,
@@ -3604,13 +3605,13 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
         if node.value:
             is_yield = isinstance(node.value, ast.Yield)
             value = self.visit(node.value)
-            tv_map = expected_type.can_assign(value, self)
-            if isinstance(tv_map, CanAssignError):
+            can_assign = expected_type.can_assign(value, self)
+            if isinstance(can_assign, CanAssignError):
                 self._show_error_if_checking(
                     node.value,
                     f"Incompatible assignment: expected {expected_type}, got {value}",
                     error_code=ErrorCode.incompatible_assignment,
-                    detail=tv_map.display(),
+                    detail=can_assign.display(),
                 )
 
             with qcore.override(
diff --git a/pyanalyze/predicates.py b/pyanalyze/predicates.py
index db3a10f..cc248d2 100644
--- a/pyanalyze/predicates.py
+++ b/pyanalyze/predicates.py
@@ -16,6 +16,7 @@ from .value import (
     KnownValue,
     MultiValuedValue,
     SubclassValue,
+    TypeVarValue,
     TypedValue,
     Value,
     unite_values,
@@ -36,6 +37,8 @@ def is_universally_assignable(value: Value, target_value: Value) -> bool:
         return all(
             is_universally_assignable(subval, target_value) for subval in value.vals
         )
+    elif isinstance(value, TypeVarValue):
+        return True
     return False
 
 
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index 1d9a62f..fb07131 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -20,11 +20,13 @@ from .stacked_scopes import (
     VarnameWithOrigin,
 )
 from .type_evaluation import ARGS, KWARGS, DEFAULT, UNKNOWN, Position
+from .typevar import resolve_bounds_map
 from .value import (
     AnnotatedValue,
     AnySource,
     AnyValue,
     AsyncTaskIncompleteValue,
+    BoundsMap,
     CallableValue,
     CanAssignContext,
     ConstraintExtension,
@@ -33,6 +35,7 @@ from .value import (
     HasAttrGuardExtension,
     KVPair,
     KnownValue,
+    LowerBound,
     MultiValuedValue,
     NoReturnConstraintExtension,
     NoReturnGuardExtension,
@@ -54,10 +57,11 @@ from .value import (
     concrete_values_from_iterable,
     extract_typevars,
     flatten_values,
+    get_tv_map,
     replace_known_sequence_value,
     stringify_object,
     unannotate_value,
-    unify_typevar_maps,
+    unify_bounds_maps,
     unite_values,
     unannotate,
     NO_RETURN_VALUE,
@@ -65,7 +69,6 @@ from .value import (
 
 import ast
 import asynq
-from collections import defaultdict
 import collections.abc
 from dataclasses import dataclass, field, replace
 import enum
@@ -578,11 +581,11 @@ class Signature:
         ctx: CheckCallContext,
         typevar_map: Optional[TypeVarMap] = None,
         is_overload: bool = False,
-    ) -> Tuple[Optional[TypeVarMap], bool, Optional[Value]]:
+    ) -> Tuple[Optional[BoundsMap], bool, Optional[Value]]:
         """Check type compatibility for a single parameter.
 
         Returns a three-tuple:
-        - A TypeVarMap if the assignment succeeded, or None if there was an error.
+        - A BoundsMap if the assignment succeeded, or None if there was an error.
         - A bool indicating whether Any was used to succeed in the assignment.
         - A Value or None, used for union decomposition with overloads.
 
@@ -592,12 +595,12 @@ class Signature:
                 param_typ = param.annotation.substitute_typevars(typevar_map)
             else:
                 param_typ = param.annotation
-            tv_map, used_any = can_assign_and_used_any(
+            bounds_map, used_any = can_assign_and_used_any(
                 param_typ, composite.value, ctx.can_assign_ctx
             )
-            if isinstance(tv_map, CanAssignError):
+            if isinstance(bounds_map, CanAssignError):
                 if composite.value is param.default:
-                    tv_map = {}
+                    bounds_map = {}
                 else:
                     if is_overload:
                         triple = decompose_union(
@@ -610,10 +613,10 @@ class Signature:
                         f" {param_typ} but got {composite.value}",
                         code=ErrorCode.incompatible_argument,
                         node=composite.node if composite.node is not None else None,
-                        detail=str(tv_map),
+                        detail=str(bounds_map),
                     )
                     return None, False, None
-            return tv_map, used_any, None
+            return bounds_map, used_any, None
         return {}, False, None
 
     def _get_positional_parameter(self, index: int) -> Optional[SigParameter]:
@@ -1080,28 +1083,30 @@ class Signature:
         return_value = self.return_value
         typevar_values: TypeVarMap = {}
         if self.all_typevars:
-            tv_possible_values: Dict[TypeVar, List[Value]] = defaultdict(list)
+            bounds_maps = []
             for param_name in self.typevars_of_params:
                 if param_name == self._return_key:
                     continue
                 param = self.parameters[param_name]
-                tv_map, _, _ = self._check_param_type_compatibility(
+                bounds_map, _, _ = self._check_param_type_compatibility(
                     param, bound_args[param_name][1], ctx
                 )
-                if tv_map is None:
+                if bounds_map is None:
                     return self.get_default_return()
                 else:
-                    # For now, the first assignment wins.
-                    for typevar, value in tv_map.items():
-                        tv_possible_values[typevar].append(value)
-            typevar_values = {
-                typevar: unite_values(
-                    *tv_possible_values.get(
-                        typevar, [AnyValue(AnySource.generic_argument)]
-                    )
+                    bounds_maps.append(bounds_map)
+            typevar_values, errors = resolve_bounds_map(
+                unify_bounds_maps(bounds_maps),
+                ctx.can_assign_ctx,
+                all_typevars=self.all_typevars,
+            )
+            if errors:
+                self.show_call_error(
+                    "Cannot resolve type variables",
+                    ctx,
+                    detail=str(CanAssignError(children=list(errors))),
                 )
-                for typevar in self.all_typevars
-            }
+                return self.get_default_return()
             if self._return_key in self.typevars_of_params:
                 return_value = return_value.substitute_typevars(typevar_values)
 
@@ -1481,7 +1486,13 @@ class Signature:
                 ]
                 new_sig = Signature.make(remaining)
                 assert isinstance(my_annotation, TypeVarValue)
-                tv_maps.append({my_annotation.typevar: CallableValue(new_sig)})
+                tv_maps.append(
+                    {
+                        my_annotation.typevar: [
+                            LowerBound(my_annotation.typevar, CallableValue(new_sig))
+                        ]
+                    }
+                )
                 consumed_paramspec = True
             elif my_param.kind is ParameterKind.ELLIPSIS:
                 consumed_paramspec = True
@@ -1518,7 +1529,7 @@ class Signature:
                 else:
                     assert False, f"unhandled param {param}"
 
-        return unify_typevar_maps(tv_maps)
+        return unify_bounds_maps(tv_maps)
 
     def can_assign_through_check_call(
         self, other: "Signature", ctx: CanAssignContext
@@ -1884,7 +1895,7 @@ def _preprocess_kwargs_no_mvv(
     elif isinstance(value, DictIncompleteValue):
         return _preprocess_kwargs_kv_pairs(value.kv_pairs, ctx)
     else:
-        mapping_tv_map = MappingValue.can_assign(value, ctx.can_assign_ctx)
+        mapping_tv_map = get_tv_map(MappingValue, value, ctx.can_assign_ctx)
         if isinstance(mapping_tv_map, CanAssignError):
             ctx.on_error(f"{value} is not a mapping", detail=str(mapping_tv_map))
             return None
@@ -2182,15 +2193,15 @@ class OverloadedSignature:
         self, other: "ConcreteSignature", ctx: CanAssignContext
     ) -> CanAssign:
         # A signature can be assigned if it can be assigned to all the component signatures.
-        tv_maps = []
+        bounds_maps = []
         for sig in self.signatures:
             can_assign = sig.can_assign(other, ctx)
             if isinstance(can_assign, CanAssignError):
                 return CanAssignError(
                     f"{other} is incompatible with overload {sig}", [can_assign]
                 )
-            tv_maps.append(can_assign)
-        return unify_typevar_maps(tv_maps)
+            bounds_maps.append(can_assign)
+        return unify_bounds_maps(bounds_maps)
 
 
 ConcreteSignature = Union[Signature, OverloadedSignature]
@@ -2243,6 +2254,9 @@ class BoundMethodSignature:
             else None,
         )
 
+    def __str__(self) -> str:
+        return f"{self.signature} bound to {self.self_composite.value}"
+
 
 @dataclass(frozen=True)
 class PropertyArgSpec:
@@ -2296,8 +2310,8 @@ MappingValue = GenericValue(collections.abc.Mapping, [TypeVarValue(K), TypeVarVa
 
 def can_assign_var_positional(
     my_param: SigParameter, args_annotation: Value, idx: int, ctx: CanAssignContext
-) -> Union[List[TypeVarMap], CanAssignError]:
-    tv_maps = []
+) -> Union[List[BoundsMap], CanAssignError]:
+    bounds_maps = []
     my_annotation = my_param.get_annotation()
     if isinstance(args_annotation, SequenceIncompleteValue):
         length = len(args_annotation.members)
@@ -2307,83 +2321,83 @@ def can_assign_var_positional(
                 f" accepts {length} values"
             )
         their_annotation = args_annotation.members[idx]
-        tv_map = their_annotation.can_assign(my_annotation, ctx)
-        if isinstance(tv_map, CanAssignError):
+        can_assign = their_annotation.can_assign(my_annotation, ctx)
+        if isinstance(can_assign, CanAssignError):
             return CanAssignError(
                 f"type of parameter {my_param.name!r} is incompatible: *args[{idx}]"
                 " type is incompatible",
-                [tv_map],
+                [can_assign],
             )
-        tv_maps.append(tv_map)
+        bounds_maps.append(can_assign)
     else:
-        tv_map = IterableValue.can_assign(args_annotation, ctx)
+        tv_map = get_tv_map(IterableValue, args_annotation, ctx)
         if isinstance(tv_map, CanAssignError):
             return CanAssignError(
                 f"{args_annotation} is not an iterable type", [tv_map]
             )
         iterable_arg = tv_map.get(T, AnyValue(AnySource.generic_argument))
-        tv_map = iterable_arg.can_assign(my_annotation, ctx)
-        if isinstance(tv_map, CanAssignError):
+        bounds_map = iterable_arg.can_assign(my_annotation, ctx)
+        if isinstance(bounds_map, CanAssignError):
             return CanAssignError(
                 f"type of parameter {my_param.name!r} is incompatible: "
                 "*args type is incompatible",
-                [tv_map],
+                [bounds_map],
             )
-        tv_maps.append(tv_map)
-    return tv_maps
+        bounds_maps.append(bounds_map)
+    return bounds_maps
 
 
 def can_assign_var_keyword(
     my_param: SigParameter, kwargs_annotation: Value, ctx: CanAssignContext
-) -> Union[List[TypeVarMap], CanAssignError]:
+) -> Union[List[BoundsMap], CanAssignError]:
     my_annotation = my_param.get_annotation()
-    tv_maps = []
+    bounds_maps = []
     if isinstance(kwargs_annotation, TypedDictValue):
         if my_param.name not in kwargs_annotation.items:
             return CanAssignError(
                 f"parameter {my_param.name!r} is not accepted by {kwargs_annotation}"
             )
         their_annotation = kwargs_annotation.items[my_param.name][1]
-        tv_map = their_annotation.can_assign(my_annotation, ctx)
-        if isinstance(tv_map, CanAssignError):
+        can_assign = their_annotation.can_assign(my_annotation, ctx)
+        if isinstance(can_assign, CanAssignError):
             return CanAssignError(
                 f"type of parameter {my_param.name!r} is incompatible:"
                 f" *kwargs[{my_param.name!r}] type is incompatible",
-                [tv_map],
+                [can_assign],
             )
-        tv_maps.append(tv_map)
+        bounds_maps.append(can_assign)
     else:
-        mapping_tv_map = MappingValue.can_assign(kwargs_annotation, ctx)
+        mapping_tv_map = get_tv_map(MappingValue, kwargs_annotation, ctx)
         if isinstance(mapping_tv_map, CanAssignError):
             return CanAssignError(
                 f"{kwargs_annotation} is not a mapping type", [mapping_tv_map]
             )
         key_arg = mapping_tv_map.get(K, AnyValue(AnySource.generic_argument))
-        tv_map = key_arg.can_assign(KnownValue(my_param.name), ctx)
-        if isinstance(tv_map, CanAssignError):
+        can_assign = key_arg.can_assign(KnownValue(my_param.name), ctx)
+        if isinstance(can_assign, CanAssignError):
             return CanAssignError(
                 f"parameter {my_param.name!r} is not accepted by **kwargs type",
-                [tv_map],
+                [can_assign],
             )
-        tv_maps.append(tv_map)
+        bounds_maps.append(can_assign)
         value_arg = mapping_tv_map.get(V, AnyValue(AnySource.generic_argument))
-        tv_map = value_arg.can_assign(my_annotation, ctx)
-        if isinstance(tv_map, CanAssignError):
+        can_assign = value_arg.can_assign(my_annotation, ctx)
+        if isinstance(can_assign, CanAssignError):
             return CanAssignError(
                 f"type of parameter {my_param.name!r} is incompatible: **kwargs type"
                 " is incompatible",
-                [tv_map],
+                [can_assign],
             )
-        tv_maps.append(tv_map)
-    return tv_maps
+        bounds_maps.append(can_assign)
+    return bounds_maps
 
 
 def decompose_union(
     expected_type: Value, parent_value: Value, ctx: CanAssignContext
-) -> Optional[Tuple[TypeVarMap, bool, Value]]:
+) -> Optional[Tuple[BoundsMap, bool, Value]]:
     value = unannotate(parent_value)
     if isinstance(value, MultiValuedValue):
-        tv_maps = []
+        bounds_maps = []
         remaining_values = []
         union_used_any = False
         for val in value.vals:
@@ -2395,11 +2409,11 @@ def decompose_union(
             else:
                 if subval_used_any:
                     union_used_any = True
-                tv_maps.append(can_assign)
-        if tv_maps:
-            tv_map = unify_typevar_maps(tv_maps)
+                bounds_maps.append(can_assign)
+        if bounds_maps:
+            bounds_map = unify_bounds_maps(bounds_maps)
             assert (
                 remaining_values
             ), f"all union members matched between {expected_type} and {parent_value}"
-            return tv_map, union_used_any, unite_values(*remaining_values)
+            return bounds_map, union_used_any, unite_values(*remaining_values)
     return None
diff --git a/pyanalyze/type_evaluation.py b/pyanalyze/type_evaluation.py
index ff750db..dca822b 100644
--- a/pyanalyze/type_evaluation.py
+++ b/pyanalyze/type_evaluation.py
@@ -36,6 +36,7 @@ from .stacked_scopes import (
 from .safe import all_of_type
 from .value import (
     NO_RETURN_VALUE,
+    BoundsMap,
     CanAssign,
     CanAssignContext,
     CanAssignError,
@@ -45,8 +46,8 @@ from .value import (
     Value,
     flatten_values,
     unannotate,
+    unify_bounds_maps,
     unite_values,
-    unify_typevar_maps,
     TypeVarMap,
 )
 
@@ -790,10 +791,10 @@ class EvaluateVisitor(ast.NodeVisitor):
 
 def decompose_union(
     expected_type: Value, parent_value: Value, ctx: CanAssignContext, exclude_any: bool
-) -> Optional[Tuple[TypeVarMap, Value]]:
+) -> Optional[Tuple[BoundsMap, Value]]:
     value = unannotate(parent_value)
     if isinstance(value, MultiValuedValue):
-        tv_maps = []
+        bounds_maps = []
         remaining_values = []
         for val in value.vals:
             can_assign = can_assign_maybe_exclude_any(
@@ -802,13 +803,13 @@ def decompose_union(
             if isinstance(can_assign, CanAssignError):
                 remaining_values.append(val)
             else:
-                tv_maps.append(can_assign)
-        if tv_maps:
-            tv_map = unify_typevar_maps(tv_maps)
+                bounds_maps.append(can_assign)
+        if bounds_maps:
+            result = unify_bounds_maps(bounds_maps)
             assert (
                 remaining_values
             ), f"all union members matched between {expected_type} and {parent_value}"
-            return tv_map, unite_values(*remaining_values)
+            return result, unite_values(*remaining_values)
     return None
 
 
diff --git a/pyanalyze/type_object.py b/pyanalyze/type_object.py
index d509332..904d132 100644
--- a/pyanalyze/type_object.py
+++ b/pyanalyze/type_object.py
@@ -11,15 +11,15 @@ from unittest import mock
 from .safe import safe_isinstance, safe_issubclass, safe_in
 from .value import (
     UNINITIALIZED_VALUE,
+    BoundsMap,
     CanAssign,
     CanAssignContext,
     CanAssignError,
     KnownValue,
-    TypeVarMap,
     TypedValue,
     Value,
     stringify_object,
-    unify_typevar_maps,
+    unify_bounds_maps,
 )
 
 
@@ -43,7 +43,7 @@ class TypeObject:
     protocol_members: Set[str] = field(default_factory=set)
     is_thrift_enum: bool = field(init=False)
     is_universally_assignable: bool = field(init=False)
-    _protocol_positive_cache: Dict[Value, TypeVarMap] = field(
+    _protocol_positive_cache: Dict[Value, BoundsMap] = field(
         default_factory=dict, repr=False
     )
 
@@ -125,14 +125,14 @@ class TypeObject:
                 return CanAssignError(
                     f"Cannot assign super object {other_val} to protocol {self}"
                 )
-            tv_map = self._protocol_positive_cache.get(other_val)
-            if tv_map is not None:
-                return tv_map
+            bounds_map = self._protocol_positive_cache.get(other_val)
+            if bounds_map is not None:
+                return bounds_map
             # This is a guard against infinite recursion if the Protocol is recursive
             if ctx.can_assume_compatibility(self, other):
                 return {}
             with ctx.assume_compatibility(self, other):
-                tv_maps = []
+                bounds_maps = []
                 for member in self.protocol_members:
                     expected = ctx.get_attribute_from_value(self_val, member)
                     # For __call__, we check compatiiblity with the other object itself.
@@ -142,13 +142,14 @@ class TypeObject:
                         actual = ctx.get_attribute_from_value(other_val, member)
                     if actual is UNINITIALIZED_VALUE:
                         return CanAssignError(f"{other} has no attribute {member!r}")
-                    tv_map = expected.can_assign(actual, ctx)
-                    if isinstance(tv_map, CanAssignError):
+                    can_assign = expected.can_assign(actual, ctx)
+                    if isinstance(can_assign, CanAssignError):
                         return CanAssignError(
-                            f"Value of protocol member {member!r} conflicts", [tv_map]
+                            f"Value of protocol member {member!r} conflicts",
+                            [can_assign],
                         )
-                    tv_maps.append(tv_map)
-            result = unify_typevar_maps(tv_maps)
+                    bounds_maps.append(can_assign)
+            result = unify_bounds_maps(bounds_maps)
             self._protocol_positive_cache[other_val] = result
             return result
 
diff --git a/pyanalyze/typeshed.py b/pyanalyze/typeshed.py
index 8d2e23e..c7727fb 100644
--- a/pyanalyze/typeshed.py
+++ b/pyanalyze/typeshed.py
@@ -12,6 +12,7 @@ from .annotations import (
     Context,
     Pep655Value,
     SyntheticEvaluator,
+    make_type_var_value,
     type_from_value,
     value_from_ast,
 )
@@ -811,7 +812,16 @@ class TypeshedFinder:
             else:
                 typevars = uniq_chain(extract_typevars(base) for base in bases)
                 if typevars:
-                    typ = GenericValue(objclass, [TypeVarValue(tv) for tv in typevars])
+                    typ = GenericValue(
+                        objclass,
+                        [
+                            make_type_var_value(
+                                tv,
+                                _AnnotationContext(finder=self, module=tv.__module__),
+                            )
+                            for tv in typevars
+                        ],
+                    )
                 else:
                     typ = TypedValue(objclass)
 
diff --git a/pyanalyze/typevar.py b/pyanalyze/typevar.py
new file mode 100644
index 0000000..f847e26
--- /dev/null
+++ b/pyanalyze/typevar.py
@@ -0,0 +1,120 @@
+"""
+
+TypeVar solver.
+
+"""
+import qcore
+from typing import Iterable, Sequence, Tuple, Union
+
+from .safe import all_of_type
+from .value import (
+    AnySource,
+    AnyValue,
+    Bound,
+    BoundsMap,
+    CanAssignContext,
+    IsOneOf,
+    LowerBound,
+    OrBound,
+    TypeVarLike,
+    TypeVarMap,
+    UpperBound,
+    Value,
+    CanAssignError,
+    unite_values,
+)
+
+
+BOTTOM = qcore.MarkerObject("<bottom>")
+TOP = qcore.MarkerObject("<top>")
+
+
+def resolve_bounds_map(
+    bounds_map: BoundsMap,
+    ctx: CanAssignContext,
+    *,
+    all_typevars: Iterable[TypeVarLike] = (),
+) -> Tuple[TypeVarMap, Sequence[CanAssignError]]:
+    tv_map = {tv: AnyValue(AnySource.generic_argument) for tv in all_typevars}
+    errors = []
+    for tv, bounds in bounds_map.items():
+        bounds = tuple(dict.fromkeys(bounds))
+        solution = solve(bounds, ctx)
+        if isinstance(solution, CanAssignError):
+            errors.append(solution)
+            solution = AnyValue(AnySource.error)
+        tv_map[tv] = solution
+    return tv_map, errors
+
+
+def solve(
+    bounds: Iterable[Bound], ctx: CanAssignContext
+) -> Union[Value, CanAssignError]:
+    bottom = BOTTOM
+    top = TOP
+    options = None
+
+    for bound in bounds:
+        if isinstance(bound, LowerBound):
+            # Ignore lower bounds to Any
+            if isinstance(bound.value, AnyValue) and bottom is not BOTTOM:
+                continue
+            if bottom is BOTTOM or bound.value.is_assignable(bottom, ctx):
+                # New bound is more specific. Adopt it.
+                bottom = bound.value
+            elif bottom.is_assignable(bound.value, ctx):
+                # New bound is less specific. Ignore it.
+                pass
+            else:
+                # New bound is separate. We have to satisfy both.
+                bottom = unite_values(bottom, bound.value)
+        elif isinstance(bound, UpperBound):
+            if top is TOP or top.is_assignable(bound.value, ctx):
+                top = bound.value
+            elif bound.value.is_assignable(top, ctx):
+                pass
+            else:
+                top = unite_values(top, bound.value)
+        elif isinstance(bound, OrBound):
+            # TODO figure out how to handle this
+            continue
+        elif isinstance(bound, IsOneOf):
+            options = bound.constraints
+        else:
+            assert False, f"unrecognized bound {bound}"
+
+    if bottom is BOTTOM:
+        if top is TOP:
+            solution = AnyValue(AnySource.generic_argument)
+        else:
+            solution = top
+    elif top is TOP:
+        solution = bottom
+    else:
+        can_assign = top.can_assign(bottom, ctx)
+        if isinstance(can_assign, CanAssignError):
+            return CanAssignError(
+                "Incompatible bounds on type variable",
+                [
+                    can_assign,
+                    CanAssignError(
+                        children=[CanAssignError(str(bound)) for bound in bounds]
+                    ),
+                ],
+            )
+        solution = bottom
+
+    if options is not None:
+        can_assigns = [option.can_assign(solution, ctx) for option in options]
+        if all_of_type(can_assigns, CanAssignError):
+            return CanAssignError(children=list(can_assigns))
+        available = [
+            option
+            for option, can_assign in zip(options, can_assigns)
+            if not isinstance(can_assign, CanAssignError)
+        ]
+        if len(available) == 1:
+            return available[0]
+        # TODO consider returning unite_values(*available) here instead.
+        return AnyValue(AnySource.inference)
+    return solution
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index 2d659b6..fd7a20a 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -20,7 +20,7 @@ these subclasses and some related utilities.
 """
 
 import collections.abc
-from collections import OrderedDict, defaultdict, deque
+from collections import OrderedDict, deque
 from dataclasses import dataclass, field, InitVar
 import enum
 import inspect
@@ -34,6 +34,7 @@ from typing import (
     ContextManager,
     Dict,
     Iterable,
+    Iterator,
     List,
     Mapping,
     Optional,
@@ -60,6 +61,7 @@ ITERATION_LIMIT = 1000
 
 TypeVarLike = Union["TypeVar", "ParamSpec"]
 TypeVarMap = Mapping[TypeVarLike, "Value"]
+BoundsMap = Mapping[TypeVarLike, Sequence["Bound"]]
 GenericBases = Mapping[Union[type, str], TypeVarMap]
 
 
@@ -88,16 +90,16 @@ class Value:
             # The bottom type is assignable to every other type.
             if other is NO_RETURN_VALUE:
                 return {}
-            tv_maps = []
+            bounds_maps = []
             for val in other.vals:
-                tv_map = self.can_assign(val, ctx)
-                if isinstance(tv_map, CanAssignError):
+                can_assign = self.can_assign(val, ctx)
+                if isinstance(can_assign, CanAssignError):
                     # Adding an additional layer here isn't helpful
-                    return tv_map
-                tv_maps.append(tv_map)
-            if not tv_maps:
+                    return can_assign
+                bounds_maps.append(can_assign)
+            if not bounds_maps:
                 return CanAssignError(f"Cannot assign {other} to {self}")
-            return unify_typevar_maps(tv_maps)
+            return unify_bounds_maps(bounds_maps)
         elif isinstance(other, (AnnotatedValue, TypeVarValue)):
             return other.can_be_assigned(self, ctx)
         elif (
@@ -283,7 +285,7 @@ class CanAssignError:
 
 
 # Return value of CanAssign
-CanAssign = Union[TypeVarMap, CanAssignError]
+CanAssign = Union[BoundsMap, CanAssignError]
 
 
 def assert_is_value(obj: object, value: Value, *, skip_annotated: bool = False) -> None:
@@ -638,16 +640,16 @@ class TypedValue(Value):
                 return {}
             return self.get_type_object(ctx).can_assign(self, other, ctx)
         elif isinstance(other, MultiValuedValue):
-            tv_maps = []
+            bounds_maps = []
             for val in other.vals:
-                tv_map = self.can_assign(val, ctx)
-                if isinstance(tv_map, CanAssignError):
+                can_assign = self.can_assign(val, ctx)
+                if isinstance(can_assign, CanAssignError):
                     # Adding an additional layer here isn't helpful
-                    return tv_map
-                tv_maps.append(tv_map)
-            if not tv_maps:
+                    return can_assign
+                bounds_maps.append(can_assign)
+            if not bounds_maps:
                 return CanAssignError(f"Cannot assign {other} to Thrift enum {self}")
-            return unify_typevar_maps(tv_maps)
+            return unify_bounds_maps(bounds_maps)
         elif isinstance(other, AnnotatedValue):
             return self.can_assign_thrift_enum(other.value, ctx)
         return CanAssignError(f"Cannot assign {other} to Thrift enum {self}")
@@ -767,15 +769,15 @@ class GenericValue(TypedValue):
             # runtime isinstance() may disagree.
             if generic_args is None or len(self.args) != len(generic_args):
                 return super().can_assign(other, ctx)
-            tv_maps = []
+            bounds_maps = []
             for i, (my_arg, their_arg) in enumerate(zip(self.args, generic_args)):
-                tv_map = my_arg.can_assign(their_arg, ctx)
-                if isinstance(tv_map, CanAssignError):
-                    return self.maybe_specify_error(i, other, tv_map, ctx)
-                tv_maps.append(tv_map)
-            if not tv_maps:
+                can_assign = my_arg.can_assign(their_arg, ctx)
+                if isinstance(can_assign, CanAssignError):
+                    return self.maybe_specify_error(i, other, can_assign, ctx)
+                bounds_maps.append(can_assign)
+            if not bounds_maps:
                 return CanAssignError(f"Cannot assign {other} to {self}")
-            return unify_typevar_maps(tv_maps)
+            return unify_bounds_maps(bounds_maps)
 
         return super().can_assign(other, ctx)
 
@@ -887,8 +889,8 @@ class SequenceIncompleteValue(GenericValue):
 
     def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign:
         if isinstance(other, SequenceIncompleteValue):
-            tv_map = self.get_type_object(ctx).can_assign(self, other, ctx)
-            if isinstance(tv_map, CanAssignError):
+            can_assign = self.get_type_object(ctx).can_assign(self, other, ctx)
+            if isinstance(can_assign, CanAssignError):
                 return CanAssignError(
                     f"Cannot assign {stringify_object(other.typ)} to"
                     f" {stringify_object(self.typ)}"
@@ -903,17 +905,17 @@ class SequenceIncompleteValue(GenericValue):
                 )
             if my_len == 0:
                 return {}  # they're both empty
-            tv_maps = []
+            bounds_maps = [can_assign]
             for i, (my_member, their_member) in enumerate(
                 zip(self.members, other.members)
             ):
-                tv_map = my_member.can_assign(their_member, ctx)
-                if isinstance(tv_map, CanAssignError):
+                can_assign = my_member.can_assign(their_member, ctx)
+                if isinstance(can_assign, CanAssignError):
                     return CanAssignError(
-                        f"Types for member {i} are incompatible", [tv_map]
+                        f"Types for member {i} are incompatible", [can_assign]
                     )
-                tv_maps.append(tv_map)
-            return unify_typevar_maps(tv_maps)
+                bounds_maps.append(can_assign)
+            return unify_bounds_maps(bounds_maps)
         return super().can_assign(other, ctx)
 
     def substitute_typevars(self, typevars: TypeVarMap) -> Value:
@@ -1074,7 +1076,7 @@ class TypedDictValue(GenericValue):
 
     def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign:
         if isinstance(other, DictIncompleteValue):
-            tv_maps = []
+            bounds_maps = []
             for key, (is_required, value) in self.items.items():
                 their_value = other.get_value(KnownValue(key), ctx)
                 if their_value is UNINITIALIZED_VALUE:
@@ -1082,41 +1084,43 @@ class TypedDictValue(GenericValue):
                         return CanAssignError(f"Key {key} is missing in {other}")
                     else:
                         continue
-                tv_map = value.can_assign(their_value, ctx)
-                if isinstance(tv_map, CanAssignError):
+                can_assign = value.can_assign(their_value, ctx)
+                if isinstance(can_assign, CanAssignError):
                     return CanAssignError(
-                        f"Types for key {key} are incompatible", children=[tv_map]
+                        f"Types for key {key} are incompatible", children=[can_assign]
                     )
-                tv_maps.append(tv_map)
-            return unify_typevar_maps(tv_maps)
+                bounds_maps.append(can_assign)
+            return unify_bounds_maps(bounds_maps)
         elif isinstance(other, TypedDictValue):
-            tv_maps = []
+            bounds_maps = []
             for key, (is_required, value) in self.items.items():
                 if key not in other.items:
                     if is_required:
                         return CanAssignError(f"Key {key} is missing in {other}")
                 else:
-                    tv_map = value.can_assign(other.items[key][1], ctx)
-                    if isinstance(tv_map, CanAssignError):
+                    can_assign = value.can_assign(other.items[key][1], ctx)
+                    if isinstance(can_assign, CanAssignError):
                         return CanAssignError(
-                            f"Types for key {key} are incompatible", children=[tv_map]
+                            f"Types for key {key} are incompatible",
+                            children=[can_assign],
                         )
-                    tv_maps.append(tv_map)
-            return unify_typevar_maps(tv_maps)
+                    bounds_maps.append(can_assign)
+            return unify_bounds_maps(bounds_maps)
         elif isinstance(other, KnownValue) and isinstance(other.val, dict):
-            tv_maps = []
+            bounds_maps = []
             for key, (is_required, value) in self.items.items():
                 if key not in other.val:
                     if is_required:
                         return CanAssignError(f"Key {key} is missing in {other}")
                 else:
-                    tv_map = value.can_assign(KnownValue(other.val[key]), ctx)
-                    if isinstance(tv_map, CanAssignError):
+                    can_assign = value.can_assign(KnownValue(other.val[key]), ctx)
+                    if isinstance(can_assign, CanAssignError):
                         return CanAssignError(
-                            f"Types for key {key} are incompatible", children=[tv_map]
+                            f"Types for key {key} are incompatible",
+                            children=[can_assign],
                         )
-                    tv_maps.append(tv_map)
-            return unify_typevar_maps(tv_maps)
+                    bounds_maps.append(can_assign)
+            return unify_bounds_maps(bounds_maps)
         return super().can_assign(other, ctx)
 
     def substitute_typevars(self, typevars: TypeVarMap) -> "TypedDictValue":
@@ -1259,7 +1263,11 @@ class SubclassValue(Value):
                     self_tobj = self.typ.get_type_object(ctx)
                     return self_tobj.can_assign(self, TypedValue(other.val), ctx)
                 elif isinstance(self.typ, TypeVarValue):
-                    return {self.typ.typevar: TypedValue(other.val)}
+                    return {
+                        self.typ.typevar: [
+                            LowerBound(self.typ.typevar, TypedValue(other.val))
+                        ]
+                    }
         elif isinstance(other, TypedValue):
             if other.typ is type:
                 return {}
@@ -1334,16 +1342,16 @@ class MultiValuedValue(Value):
         if is_union(other):
             if other is NO_RETURN_VALUE:
                 return {}
-            tv_maps = []
+            bounds_maps = []
             for val in flatten_values(other):
-                tv_map = self.can_assign(val, ctx)
-                if isinstance(tv_map, CanAssignError):
+                can_assign = self.can_assign(val, ctx)
+                if isinstance(can_assign, CanAssignError):
                     # Adding an additional layer here isn't helpful
-                    return tv_map
-                tv_maps.append(tv_map)
-            if not tv_maps:
+                    return can_assign
+                bounds_maps.append(can_assign)
+            if not bounds_maps:
                 return CanAssignError(f"Cannot assign {other} to {self}")
-            return unify_typevar_maps(tv_maps)
+            return unify_bounds_maps(bounds_maps)
         elif isinstance(other, AnyValue) and not ctx.should_exclude_any():
             ctx.record_any_used()
             return {}
@@ -1379,26 +1387,18 @@ class MultiValuedValue(Value):
                                 if not isinstance(subval, KnownValue)
                             ]
 
-            tv_maps = []
+            bounds_maps = []
             errors = []
             for val in my_vals:
-                tv_map = val.can_assign(other, ctx)
-                if isinstance(tv_map, CanAssignError):
-                    errors.append(tv_map)
+                can_assign = val.can_assign(other, ctx)
+                # Ignore any branches that don't match
+                if isinstance(can_assign, CanAssignError):
+                    errors.append(can_assign)
                 else:
-                    tv_maps.append(tv_map)
-            # Ignore any branches that don't match
-            if not tv_maps:
+                    bounds_maps.append(can_assign)
+            if not bounds_maps:
                 return CanAssignError("Cannot assign to Union", errors)
-            # Include only typevars that appear in all branches; i.e., prefer
-            # branches that don't set typevars.
-            typevars = collections.Counter(tv for tv_map in tv_maps for tv in tv_map)
-            num_tv_maps = len(tv_maps)
-            return {
-                tv: unite_values(*[tv_map[tv] for tv_map in tv_maps])
-                for tv, count in typevars.items()
-                if count == num_tv_maps
-            }
+            return intersect_bounds_maps(bounds_maps)
 
     def get_type_value(self) -> Value:
         if not self.vals:
@@ -1479,6 +1479,46 @@ class ReferencingValue(Value):
 SelfT = TypeVar("SelfT")
 
 
+@dataclass(frozen=True)
+class Bound:
+    pass
+
+
+@dataclass(frozen=True)
+class LowerBound(Bound):
+    """LowerBound(T, V) means V must be assignable to the value of T."""
+
+    typevar: TypeVarLike
+    value: Value
+
+    def __str__(self) -> str:
+        return f"{self.value} <= {self.typevar}"
+
+
+@dataclass(frozen=True)
+class UpperBound(Bound):
+    """UpperBound(T, V) means the value of T must be assignable to V."""
+
+    typevar: TypeVarLike
+    value: Value
+
+    def __str__(self) -> str:
+        return f"{self.value} >= {self.typevar}"
+
+
+@dataclass(frozen=True)
+class OrBound(Bound):
+    """At least one of the specified bounds must be true."""
+
+    bounds: Sequence[Sequence[Bound]]
+
+
+@dataclass(frozen=True)
+class IsOneOf(Bound):
+    typevar: TypeVarLike
+    constraints: Sequence[Value]
+
+
 @dataclass(frozen=True)
 class TypeVarValue(Value):
     """Value representing a ``typing.TypeVar`` or ``typing.ParamSpec``.
@@ -1495,65 +1535,38 @@ class TypeVarValue(Value):
     def substitute_typevars(self, typevars: TypeVarMap) -> Value:
         return typevars.get(self.typevar, self)
 
+    def get_inherent_bounds(self) -> Iterator[Bound]:
+        if self.bound is not None:
+            yield UpperBound(self.typevar, self.bound)
+        if self.constraints:
+            yield IsOneOf(self.typevar, self.constraints)
+
     def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign:
-        if self == other or (isinstance(other, AnnotatedValue) and self == other.value):
+        if self == other:
             return {}
-        if self.bound is not None:
-            can_assign = self.bound.can_assign(other, ctx)
-            if isinstance(can_assign, CanAssignError):
-                return CanAssignError(
-                    f"Value of TypeVar {self} cannot be {other}", [can_assign]
-                )
-            return {**can_assign, self.typevar: other}
-        elif self.constraints:
-            can_assigns = [
-                constraint.can_assign(other, ctx) for constraint in self.constraints
-            ]
-            if all_of_type(can_assigns, CanAssignError):
-                return CanAssignError(f"Cannot assign to {self}", list(can_assigns))
-            possibilities = [
-                constraint
-                for constraint, can_assign in zip(self.constraints, can_assigns)
-                if not isinstance(can_assign, CanAssignError)
-            ]
-            if len(possibilities) == 1:
-                (solution,) = possibilities
-            else:
-                # Inferring something else produces too many issues for now.
-                solution = AnyValue(AnySource.inference)
-            return {self.typevar: solution}
-        return {self.typevar: other}
+        if isinstance(other, TypeVarValue):
+            bounds = [*self.get_inherent_bounds(), *other.get_inherent_bounds()]
+        else:
+            bounds = [LowerBound(self.typevar, other), *self.get_inherent_bounds()]
+        return self.make_bounds_map(bounds, other, ctx)
 
     def can_be_assigned(self, left: Value, ctx: CanAssignContext) -> CanAssign:
         if left == self:
             return {}
-        if self.bound is not None:
-            # TODO not sure this is right, but it helps test cases in
-            # test_annotations.py behave as expected.
-            can_assign = self.bound.can_assign(left, ctx)
-            if isinstance(can_assign, CanAssignError):
-                return CanAssignError(
-                    f"Value of TypeVar {self} cannot be {left}", [can_assign]
-                )
-            return {**can_assign, self.typevar: left}
-        elif self.constraints:
-            can_assigns = [
-                left.can_assign(constraint, ctx) for constraint in self.constraints
-            ]
-            if all_of_type(can_assigns, CanAssignError):
-                return CanAssignError(f"Cannot assign to {self}", list(can_assigns))
-            possibilities = [
-                constraint
-                for constraint, can_assign in zip(self.constraints, can_assigns)
-                if not isinstance(can_assign, CanAssignError)
-            ]
-            if len(possibilities) == 1:
-                (solution,) = possibilities
-            else:
-                # Inferring something else produces too many issues for now.
-                solution = AnyValue(AnySource.inference)
-            return {self.typevar: solution}
-        return {self.typevar: left}
+        if isinstance(left, TypeVarValue):
+            bounds = [*self.get_inherent_bounds(), *left.get_inherent_bounds()]
+        else:
+            bounds = [UpperBound(self.typevar, left), *self.get_inherent_bounds()]
+        return self.make_bounds_map(bounds, left, ctx)
+
+    def make_bounds_map(
+        self, bounds: Sequence[Bound], other: Value, ctx: CanAssignContext
+    ) -> CanAssign:
+        bounds_map = {self.typevar: bounds}
+        _, errors = pyanalyze.typevar.resolve_bounds_map(bounds_map, ctx)
+        if errors:
+            return CanAssignError(f"Value of {self} cannot be {other}", list(errors))
+        return bounds_map
 
     def get_fallback_value(self) -> Value:
         if self.bound is not None:
@@ -1581,7 +1594,7 @@ def set_self(value: Value, self_value: Value) -> Value:
     return value.substitute_typevars({SelfT: self_value})
 
 
-@dataclass
+@dataclass(frozen=True)
 class ParamSpecArgsValue(Value):
     param_spec: ParamSpec
 
@@ -1589,7 +1602,7 @@ class ParamSpecArgsValue(Value):
         return f"{self.param_spec}.args"
 
 
-@dataclass
+@dataclass(frozen=True)
 class ParamSpecKwargsValue(Value):
     param_spec: ParamSpec
 
@@ -1836,25 +1849,25 @@ class AnnotatedValue(Value):
         can_assign = self.value.can_assign(other, ctx)
         if isinstance(can_assign, CanAssignError):
             return can_assign
-        tv_maps = [can_assign]
+        bounds_maps = [can_assign]
         for custom_check in self.get_metadata_of_type(CustomCheckExtension):
             custom_can_assign = custom_check.custom_check.can_assign(other, ctx)
             if isinstance(custom_can_assign, CanAssignError):
                 return custom_can_assign
-            tv_maps.append(custom_can_assign)
-        return unify_typevar_maps(tv_maps)
+            bounds_maps.append(custom_can_assign)
+        return unify_bounds_maps(bounds_maps)
 
     def can_be_assigned(self, other: Value, ctx: CanAssignContext) -> CanAssign:
         can_assign = other.can_assign(self.value, ctx)
         if isinstance(can_assign, CanAssignError):
             return can_assign
-        tv_maps = [can_assign]
+        bounds_maps = [can_assign]
         for custom_check in self.get_metadata_of_type(CustomCheckExtension):
             custom_can_assign = custom_check.custom_check.can_be_assigned(other, ctx)
             if isinstance(custom_can_assign, CanAssignError):
                 return custom_can_assign
-            tv_maps.append(custom_can_assign)
-        return unify_typevar_maps(tv_maps)
+            bounds_maps.append(custom_can_assign)
+        return unify_bounds_maps(bounds_maps)
 
     def walk_values(self) -> Iterable[Value]:
         yield self
@@ -1971,12 +1984,38 @@ def flatten_values(val: Value, *, unwrap_annotated: bool = False) -> Iterable[Va
         yield val
 
 
-def unify_typevar_maps(tv_maps: Sequence[TypeVarMap]) -> TypeVarMap:
-    raw_map = defaultdict(list)
-    for tv_map in tv_maps:
-        for tv, value in tv_map.items():
-            raw_map[tv].append(value)
-    return {tv: unite_values(*values) for tv, values in raw_map.items()}
+def get_tv_map(
+    left: Value, right: Value, ctx: CanAssignContext
+) -> Union[TypeVarMap, CanAssignError]:
+    bounds_map = left.can_assign(right, ctx)
+    if isinstance(bounds_map, CanAssignError):
+        return bounds_map
+    tv_map, errors = pyanalyze.typevar.resolve_bounds_map(bounds_map, ctx)
+    if errors:
+        return CanAssignError(children=list(errors))
+    return tv_map
+
+
+def unify_bounds_maps(bounds_maps: Sequence[BoundsMap]) -> BoundsMap:
+    result = {}
+    for bounds_map in bounds_maps:
+        for tv, bounds in bounds_map.items():
+            result.setdefault(tv, []).extend(bounds)
+    return result
+
+
+def intersect_bounds_maps(bounds_maps: Sequence[BoundsMap]) -> BoundsMap:
+    intermediate: Dict[TypeVarLike, Set[Tuple[Bound, ...]]] = {}
+    for bounds_map in bounds_maps:
+        for tv, bounds in bounds_map.items():
+            intermediate.setdefault(tv, set()).add(tuple(bounds))
+    return {
+        tv: [OrBound(tuple(bound_lists))]
+        if len(bound_lists) > 1
+        else next(iter(bound_lists))
+        for tv, bound_lists in intermediate.items()
+        if all(tv in bounds_map for bounds_map in bounds_maps)
+    }
 
 
 def make_weak(val: Value) -> Value:
@@ -2160,11 +2199,11 @@ def concrete_values_from_iterable(
             is_nonempty = True
     elif value is NO_RETURN_VALUE:
         return NO_RETURN_VALUE
-    iter_tv_map = IterableValue.can_assign(value, ctx)
+    iter_tv_map = get_tv_map(IterableValue, value, ctx)
     if not isinstance(iter_tv_map, CanAssignError):
         val = iter_tv_map.get(T, AnyValue(AnySource.generic_argument))
     else:
-        getitem_tv_map = GetItemProtoValue.can_assign(value, ctx)
+        getitem_tv_map = get_tv_map(GetItemProtoValue, value, ctx)
         if not isinstance(getitem_tv_map, CanAssignError):
             val = getitem_tv_map.get(T, AnyValue(AnySource.generic_argument))
         else:
@@ -2212,7 +2251,7 @@ def kv_pairs_from_mapping(
             for key, (required, value) in value_val.items.items()
         ]
     else:
-        can_assign = MappingValue.can_assign(value_val, ctx)
+        can_assign = get_tv_map(MappingValue, value_val, ctx)
         if isinstance(can_assign, CanAssignError):
             return can_assign
         key_type = can_assign.get(K, AnyValue(AnySource.generic_argument))
@@ -2284,7 +2323,7 @@ def unpack_values(
             if not isinstance(vals, CanAssignError):
                 return vals
 
-    tv_map = IterableValue.can_assign(value, ctx)
+    tv_map = get_tv_map(IterableValue, value, ctx)
     if isinstance(tv_map, CanAssignError):
         return tv_map
     iterable_type = tv_map.get(T, AnyValue(AnySource.generic_argument))


Test patch:
diff --git a/pyanalyze/test_typevar.py b/pyanalyze/test_typevar.py
index e7710ad..2779b15 100644
--- a/pyanalyze/test_typevar.py
+++ b/pyanalyze/test_typevar.py
@@ -158,3 +158,75 @@ class TestTypeVar(TestNameCheckVisitorBase):
         def capybara(d: Dict[str, str], key: str) -> None:
             assert_is_value(dictget(d, key), TypedValue(str) | KnownValue(None))
             assert_is_value(dictget(d, key, 1), TypedValue(str) | KnownValue(1))
+
+
+class TestSolve(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_filter_like(self):
+        from typing import Callable, TypeVar
+
+        T = TypeVar("T")
+
+        def callable(o: object) -> bool:
+            return True
+
+        def filterish(func: Callable[[T], bool], data: T) -> T:
+            return data
+
+        def capybara():
+            assert_is_value(filterish(callable, 1), KnownValue(1))
+
+    @assert_passes()
+    def test_one_any(self):
+        from typing import TypeVar
+
+        T = TypeVar("T")
+
+        def sub(x: T, y: T) -> T:
+            return x
+
+        def capybara(unannotated):
+            assert_is_value(sub(1, unannotated), KnownValue(1))
+
+    @assert_passes()
+    def test_isinstance(self):
+        from typing import TypeVar
+
+        AnyStr = TypeVar("AnyStr", str, bytes)
+
+        class StrSub(str):
+            pass
+
+        def want_str(s: StrSub) -> None:
+            pass
+
+        def take_tv(t: AnyStr) -> AnyStr:
+            if isinstance(t, StrSub):
+                assert_is_value(t, TypedValue(StrSub))
+                return t
+            else:
+                return t
+
+    @assert_passes()
+    def test_tv_union(self):
+        from typing import TypeVar, Union
+
+        AnyStr = TypeVar("AnyStr", str, bytes)
+
+        def take_seq(seq: AnyStr) -> AnyStr:
+            return seq
+
+        def take_union(seq: Union[bytes, str]) -> None:
+            take_seq(seq)  # E: incompatible_argument
+
+    @assert_passes()
+    def test_tv_sequence(self):
+        from typing import TypeVar, Sequence, Union
+
+        AnyStr = TypeVar("AnyStr", bound=Union[str, bytes])
+
+        def take_seq(seq: Sequence[AnyStr]) -> Sequence[AnyStr]:
+            return seq
+
+        def take_union(seq: Union[Sequence[bytes], Sequence[str]]) -> None:
+            take_seq(seq)