text: import itertools
  from typing import Callable, List
  
  from vyper import ast as vy_ast
  from vyper.context import types
  from vyper.context.namespace import get_namespace
  from vyper.context.types.indexable.sequence import (
      ArrayDefinition,
      TupleDefinition,
  )
  from vyper.context.types.value.boolean import BoolDefinition
  from vyper.exceptions import (
      ArrayIndexException,
      InvalidLiteral,
      InvalidOperation,
      InvalidReference,
      InvalidType,
      OverflowException,
      StructureException,
      TypeMismatch,
      UndeclaredDefinition,
      UnknownAttribute,
      VyperException,
      ZeroDivisionException,
  )
  
  
  def _validate_op(node, types_list, validation_fn_name):
      if not types_list:
          raise TypeMismatch(f"Cannot perform {node.op.description} between dislike types", node)
  
      if len(types_list) == 1:
          getattr(types_list[0], validation_fn_name)(node)
          return types_list
  
      for type_ in types_list.copy():
          try:
              getattr(type_, validation_fn_name)(node)
          except InvalidOperation:
              types_list.remove(type_)
  
      if types_list:
          return types_list
      raise InvalidOperation(f"Cannot perform {node.op.description} on value", node)
  
  
  class _ExprTypeChecker:
      """
      Node type-checker class.
  
      Type-check logic is implemented in `type_from_<NODE_CLASS>` methods, organized
      according to the Vyper ast node class. Calls to `get_exact_type_from_node` and
      `get_possible_types_from_node` are forwarded to this class, where the node
      class's method resolution order is examined to decide which method to call.
      """
  
      def __init__(self):
          self.namespace = get_namespace()
  
      def get_exact_type_from_node(self, node):
          fn = self._find_fn(node)
          types_list = fn(node)
  
          if len(types_list) > 1:
              raise StructureException("Ambiguous type", node)
          return types_list[0]
  
      def get_possible_types_from_node(self, node):
          fn = self._find_fn(node)
          return fn(node)
  
      def _find_fn(self, node):
          # look for a type-check method for each class in the given class mro
          for name in [i.__name__ for i in type(node).mro()]:
              if name == "VyperNode":
                  break
              fn = getattr(self, f"types_from_{name}", None)
              if fn is not None:
                  return fn
  
          raise StructureException("Cannot determine type of this object", node)
  
      def types_from_Attribute(self, node):
          # variable attribute, e.g. `foo.bar`
          var = self.get_exact_type_from_node(node.value)
          name = node.attr
          try:
              return [var.get_member(name, node)]
          except UnknownAttribute:
              if node.get("value.id") != "self":
                  raise
              if name in self.namespace:
                  raise InvalidReference(
                      f"'{name}' is not a storage variable, it should not be prepended with self",
                      node,
                  ) from None
              raise UndeclaredDefinition(
                  f"Storage variable '{name}' has not been declared", node
              ) from None
  
      def types_from_BinOp(self, node):
          # binary operation: `x + y`
          types_list = get_common_types(node.left, node.right)
  
          if (
              isinstance(node.op, (vy_ast.Div, vy_ast.Mod))
              and isinstance(node.right, vy_ast.Num)
              and not node.right.value
          ):
              raise ZeroDivisionException(f"{node.op.description} by zero", node)
  
          return _validate_op(node, types_list, "validate_numeric_op")
  
      def types_from_BoolOp(self, node):
          # boolean operation: `x and y`
          types_list = get_common_types(*node.values)
          _validate_op(node, types_list, "validate_boolean_op")
          return [BoolDefinition()]
  
      def types_from_Compare(self, node):
          # comparison: `x < y`
          if isinstance(node.op, vy_ast.In):
              # x in y
              left = self.get_possible_types_from_node(node.left)
              right = self.get_possible_types_from_node(node.right)
              if next((i for i in left if isinstance(i, ArrayDefinition)), False):
                  raise InvalidOperation(
                      "Left operand in membership comparison cannot be Array type", node.left,
                  )
              if next((i for i in right if not isinstance(i, ArrayDefinition)), False):
                  raise InvalidOperation(
                      "Right operand must be Array for membership comparison", node.right
                  )
              types_list = [i for i in left if _is_type_in_list(i, [i.value_type for i in right])]
              if not types_list:
                  raise TypeMismatch(
                      "Cannot perform membership comparison between dislike types", node
                  )
          else:
              types_list = get_common_types(node.left, node.right)
              _validate_op(node, types_list, "validate_comparator")
          return [BoolDefinition()]
  
      def types_from_Call(self, node):
          # function calls, e.g. `foo()`
          var = self.get_exact_type_from_node(node.func)
          return_value = var.fetch_call_return(node)
          if return_value:
              return [return_value]
          raise InvalidType(f"{var} did not return a value", node)
  
      def types_from_Constant(self, node):
          # literal value (integer, string, etc)
          types_list = []
          for primitive in types.get_primitive_types().values():
              try:
                  obj = primitive.from_literal(node)
                  types_list.append(obj)
              except VyperException:
                  continue
          if types_list:
              return types_list
  
          if isinstance(node, vy_ast.Num):
              raise OverflowException(
                  "Numeric literal is outside of allowable range for number types", node,
              )
          raise InvalidLiteral(f"Could not determine type for literal value '{node.value}'", node)
  
      def types_from_List(self, node):
          # literal array
          if not node.elements:
              raise InvalidLiteral("Cannot have an empty array", node)
  
          types_list = get_common_types(*node.elements)
          if types_list:
              return [ArrayDefinition(i, len(node.elements)) for i in types_list]
  
          raise InvalidLiteral("Array contains multiple, incompatible types", node)
  
      def types_from_Name(self, node):
          # variable name, e.g. `foo`
          name = node.id
          if name not in self.namespace and name in self.namespace["self"].members:
              raise InvalidReference(
                  f"'{name}' is a storage variable, access it as self.{name}", node,
              )
          try:
              return [self.namespace[node.id]]
          except VyperException as exc:
              raise exc.with_annotation(node) from None
  
      def types_from_Subscript(self, node):
          # index access, e.g. `foo[1]`
          base_type = self.get_exact_type_from_node(node.value)
          return [base_type.get_index_type(node.slice.value)]
  
      def types_from_Tuple(self, node):
          types_list = [self.get_exact_type_from_node(i) for i in node.elements]
          return [TupleDefinition(types_list)]
  
      def types_from_UnaryOp(self, node):
          # unary operation: `-foo`
          types_list = self.get_possible_types_from_node(node.operand)
          return _validate_op(node, types_list, "validate_numeric_op")
  
  
  def _is_type_in_list(obj, types_list):
      # check if a type object is in a list of types
      return next((True for i in types_list if i.compare_type(obj)), False)
  
  
  def _filter(type_, fn_name, node):
      # filter function used when evaluating boolean ops and comparators
      try:
          getattr(type_, fn_name)(node)
          return True
      except InvalidOperation:
          return False
  
  
  def get_possible_types_from_node(node):
      """
      Return a list of possible types for the given node.
  
      Raises if no possible types can be found.
  
      Arguments
      ---------
      node : VyperNode
          A vyper ast node.
  
      Returns
      -------
      List
          List of one or more BaseType objects.
      """
      return _ExprTypeChecker().get_possible_types_from_node(node)
  
  
  def get_exact_type_from_node(node):
      """
      Return exactly one type for a given node.
  
      Raises if there is more than one possible type.
  
      Arguments
      ---------
      node : VyperNode
          A vyper ast node.
  
      Returns
      -------
      BaseType
          Type object.
      """
  
      return _ExprTypeChecker().get_exact_type_from_node(node)
  
  
  def get_common_types(*nodes: vy_ast.VyperNode, filter_fn: Callable = None) -> List:
      """
      Return a list of common possible types between one or more nodes.
  
      Arguments
      ---------
      *nodes : VyperNode
          Vyper ast nodes.
      filter_fn : Callable, optional
          If given, results are filtered by this function prior to returning.
  
      Returns
      -------
      list
          List of zero or more `BaseType` objects.
      """
      common_types = _ExprTypeChecker().get_possible_types_from_node(nodes[0])
  
      for item in nodes[1:]:
          new_types = _ExprTypeChecker().get_possible_types_from_node(item)
  
          common = [i for i in common_types if _is_type_in_list(i, new_types)]
          rejected = [i for i in common_types if i not in common]
          common += [i for i in new_types if _is_type_in_list(i, rejected)]
  
          common_types = common
  
      if filter_fn is not None:
          common_types = [i for i in common_types if filter_fn(i)]
  
      return common_types
  
  
  def _validate_literal_array(node, expected):
      # validate that every item within an array has the same type
      if len(node.elements) != expected.length:
          return False
  
      for item in node.elements:
          try:
              validate_expected_type(item, expected.value_type)
          except (InvalidType, TypeMismatch):
              return False
  
      return True
  
  
  def validate_expected_type(node, expected_type):
      """
      Validate that the given node matches the expected type(s)
  
      Raises if the node does not match one of the expected types.
  
      Arguments
      ---------
      node : VyperNode
          Vyper ast node.
      expected_type : Tuple | BaseType
          A type object, or tuple of type objects
  
      Returns
      -------
      None
      """
      given_types = _ExprTypeChecker().get_possible_types_from_node(node)
      if not isinstance(expected_type, tuple):
          expected_type = (expected_type,)
  
      if isinstance(node, (vy_ast.List, vy_ast.Tuple)):
          # special case - for literal arrays or tuples we individually validate each item
          for expected in (i for i in expected_type if isinstance(i, ArrayDefinition)):
              if _validate_literal_array(node, expected):
                  return
      else:
          for given, expected in itertools.product(given_types, expected_type):
              if expected.compare_type(given):
                  return
  
      # validation failed, prepare a meaningful error message
      if len(expected_type) > 1:
          expected_str = f"one of {', '.join(str(i) for i in expected_type)}"
      else:
          expected_str = expected_type[0]
  
      if len(given_types) == 1 and getattr(given_types[0], "_is_callable", False):
          raise StructureException(
              f"{given_types[0]} cannot be referenced directly, it must be called", node
          )
  
      if not isinstance(node, (vy_ast.List, vy_ast.Tuple)) and node.get_descendants(
          vy_ast.Name, include_self=True
      ):
          raise TypeMismatch(
              f"Given reference has type {given_types[0]}, expected {expected_str}", node
          )
      else:
          if len(given_types) == 1:
              given_str = str(given_types[0])
          else:
              types_str = sorted(str(i) for i in given_types)
              given_str = f"{', '.join(types_str[:1])} or {types_str[-1]}"
  
          raise InvalidType(
              f"Expected {expected_str} but literal can only be cast as {given_str}", node
          )
  
  
  def get_index_value(node: vy_ast.Index) -> int:
      """
      Return the literal value for a `Subscript` index.
  
      Arguments
      ---------
      node : vy_ast.Index
          Vyper ast node from the `slice` member of a Subscript node. Must be an
          `Index` object (Vyper does not support `Slice` or `ExtSlice`).
  
      Returns
      -------
      int
          Literal integer value.
      """
  
      if not isinstance(node.get("value"), vy_ast.Int):
          raise InvalidType("Subscript must be a literal integer", node)
  
      if node.value.value <= 0:
          raise ArrayIndexException("Subscript must be greater than 0", node)
  
      return node.value.value
  