text: #
  # Copyright (c) 2017 Intel Corporation
  # SPDX-License-Identifier: BSD-2-Clause
  #
  
  import numpy
  import operator
  from numba.core import types, ir, config, cgutils, errors
  from numba.core.ir_utils import (
      mk_unique_var,
      find_topo_order,
      dprint_func_ir,
      get_global_func_typ,
      guard,
      require,
      get_definition,
      find_callname,
      find_build_sequence,
      find_const,
      is_namedtuple_class,
      build_definitions,
      find_potential_aliases,
      get_canonical_alias,
      GuardException,
  )
  from numba.core.analysis import compute_cfg_from_blocks
  from numba.core.typing import npydecl, signature
  import copy
  from numba.core.extending import intrinsic, register_jitable
  import llvmlite
  from numba.np.unsafe.ndarray import to_fixed_tuple
  
  UNKNOWN_CLASS = -1
  CONST_CLASS = 0
  MAP_TYPES = [numpy.ufunc]
  
  array_analysis_extensions = {}
  
  # declaring call classes
  array_creation = ["empty", "zeros", "ones", "full"]
  
  random_int_args = ["rand", "randn"]
  
  random_1arg_size = [
      "ranf",
      "random_sample",
      "sample",
      "random",
      "standard_normal",
  ]
  
  random_2arg_sizelast = [
      "chisquare",
      "weibull",
      "power",
      "geometric",
      "exponential",
      "poisson",
      "rayleigh",
  ]
  
  random_3arg_sizelast = [
      "normal",
      "uniform",
      "beta",
      "binomial",
      "f",
      "gamma",
      "lognormal",
      "laplace",
  ]
  
  random_calls = (
      random_int_args
      + random_1arg_size
      + random_2arg_sizelast
      + random_3arg_sizelast
      + ["randint", "triangular"]
  )
  
  
  @intrinsic
  def wrap_index(typingctx, idx, size):
      """
      Calculate index value "idx" relative to a size "size" value as
      (idx % size), where "size" is known to be positive.
      Note that we use the mod(%) operation here instead of
      (idx < 0 ? idx + size : idx) because we may have situations
      where idx > size due to the way indices are calculated
      during slice/range analysis.
  
      Both idx and size have to be Integer types.
      size should be from the array size vars that array_analysis
      adds and the bitwidth should match the platform maximum.
      """
      require(isinstance(idx, types.scalars.Integer))
      require(isinstance(size, types.scalars.Integer))
  
      # We need both idx and size to be platform size so that we can compare.
      unified_ty = types.intp if size.signed else types.uintp
      idx_unified = types.intp if idx.signed else types.uintp
  
      def codegen(context, builder, sig, args):
          ll_idx_unified_ty = context.get_data_type(idx_unified)
          ll_unified_ty = context.get_data_type(unified_ty)
          if idx_unified.signed:
              idx = builder.sext(args[0], ll_idx_unified_ty)
          else:
              idx = builder.zext(args[0], ll_idx_unified_ty)
          if unified_ty.signed:
              size = builder.sext(args[1], ll_unified_ty)
          else:
              size = builder.zext(args[1], ll_unified_ty)
          neg_size = builder.neg(size)
          zero = llvmlite.ir.Constant(ll_unified_ty, 0)
          # If idx is unsigned then these signed comparisons will fail in those
          # cases where the idx has the highest bit set, namely more than 2**63
          # on 64-bit platforms.
          idx_negative = builder.icmp_signed("<", idx, zero)
          pos_oversize = builder.icmp_signed(">=", idx, size)
          neg_oversize = builder.icmp_signed("<=", idx, neg_size)
          pos_res = builder.select(pos_oversize, size, idx)
          neg_res = builder.select(neg_oversize, zero, builder.add(idx, size))
          mod = builder.select(idx_negative, neg_res, pos_res)
          return mod
  
      return signature(unified_ty, idx, size), codegen
  
  
  def wrap_index_literal(idx, size):
      if idx < 0:
          if idx <= -size:
              return 0
          else:
              return idx + size
      else:
          if idx >= size:
              return size
          else:
              return idx
  
  
  @intrinsic
  def assert_equiv(typingctx, *val):
      """
      A function that asserts the inputs are of equivalent size,
      and throws runtime error when they are not. The input is
      a vararg that contains an error message, followed by a set
      of objects of either array, tuple or integer.
      """
      if len(val) > 1:
          # Make sure argument is a single tuple type. Note that this only
          # happens when IR containing assert_equiv call is being compiled
          # (and going through type inference) again.
          val = (types.StarArgTuple(val),)
  
      assert len(val[0]) > 1
      # Arguments must be either array, tuple, or integer
      assert all(
          isinstance(a, (
              types.ArrayCompatible,
              types.BaseTuple,
              types.SliceType,
              types.Integer
          ))
          for a in val[0][1:]
      )
      if not isinstance(val[0][0], types.StringLiteral):
          raise errors.TypingError('first argument must be a StringLiteral')
  
      def codegen(context, builder, sig, args):
          assert len(args) == 1  # it is a vararg tuple
          tup = cgutils.unpack_tuple(builder, args[0])
          tup_type = sig.args[0]
          msg = sig.args[0][0].literal_value
  
          def unpack_shapes(a, aty):
              if isinstance(aty, types.ArrayCompatible):
                  ary = context.make_array(aty)(context, builder, a)
                  return cgutils.unpack_tuple(builder, ary.shape)
              elif isinstance(aty, types.BaseTuple):
                  return cgutils.unpack_tuple(builder, a)
              else:  # otherwise it is a single integer
                  return [a]
  
          def pairwise(a, aty, b, bty):
              ashapes = unpack_shapes(a, aty)
              bshapes = unpack_shapes(b, bty)
              assert len(ashapes) == len(bshapes)
              for (m, n) in zip(ashapes, bshapes):
                  m_eq_n = builder.icmp_unsigned('==', m, n)
                  with builder.if_else(m_eq_n) as (then, orelse):
                      with then:
                          pass
                      with orelse:
                          context.call_conv.return_user_exc(
                              builder, AssertionError, (msg,)
                          )
  
          for i in range(1, len(tup_type) - 1):
              pairwise(tup[i], tup_type[i], tup[i + 1], tup_type[i + 1])
          r = context.get_constant_generic(builder, types.NoneType, None)
          return r
  
      return signature(types.none, *val), codegen
  
  
  class EquivSet(object):
  
      """EquivSet keeps track of equivalence relations between
      a set of objects.
      """
  
      def __init__(self, obj_to_ind=None, ind_to_obj=None, next_ind=0):
          """Create a new EquivSet object. Optional keyword arguments are for
          internal use only.
          """
          # obj_to_ind maps object to equivalence index (sometimes also called
          # equivalence class) is a non-negative number that uniquely identifies
          # a set of objects that are equivalent.
          self.obj_to_ind = obj_to_ind if obj_to_ind else {}
          # ind_to_obj maps equivalence index to a list of objects.
          self.ind_to_obj = ind_to_obj if ind_to_obj else {}
          # next index number that is incremented each time a new equivalence
          # relation is created.
          self.next_ind = next_ind
  
      def empty(self):
          """Return an empty EquivSet object.
          """
          return EquivSet()
  
      def clone(self):
          """Return a new copy.
          """
          return EquivSet(
              obj_to_ind=copy.deepcopy(self.obj_to_ind),
              ind_to_obj=copy.deepcopy(self.ind_to_obj),
              next_id=self.next_ind,
          )
  
      def __repr__(self):
          return "EquivSet({})".format(self.ind_to_obj)
  
      def is_empty(self):
          """Return true if the set is empty, or false otherwise.
          """
          return self.obj_to_ind == {}
  
      def _get_ind(self, x):
          """Return the internal index (greater or equal to 0) of the given
          object, or -1 if not found.
          """
          return self.obj_to_ind.get(x, -1)
  
      def _get_or_add_ind(self, x):
          """Return the internal index (greater or equal to 0) of the given
          object, or create a new one if not found.
          """
          if x in self.obj_to_ind:
              i = self.obj_to_ind[x]
          else:
              i = self.next_ind
              self.next_ind += 1
          return i
  
      def _insert(self, objs):
          """Base method that inserts a set of equivalent objects by modifying
          self.
          """
          assert len(objs) > 1
  
          inds = tuple(self._get_or_add_ind(x) for x in objs)
          ind = min(inds)
  
          if config.DEBUG_ARRAY_OPT >= 2:
              print("_insert:", objs, inds)
  
          if not (ind in self.ind_to_obj):
              self.ind_to_obj[ind] = []
  
          for i, obj in zip(inds, objs):
              if i == ind:
                  if not (obj in self.ind_to_obj[ind]):
                      self.ind_to_obj[ind].append(obj)
                      self.obj_to_ind[obj] = ind
              else:
                  if i in self.ind_to_obj:
                      # those already existing are reassigned
                      for x in self.ind_to_obj[i]:
                          self.obj_to_ind[x] = ind
                          self.ind_to_obj[ind].append(x)
                      del self.ind_to_obj[i]
                  else:
                      # those that are new are assigned.
                      self.obj_to_ind[obj] = ind
                      self.ind_to_obj[ind].append(obj)
  
      def is_equiv(self, *objs):
          """Try to derive if given objects are equivalent, return true
          if so, or false otherwise.
          """
          inds = [self._get_ind(x) for x in objs]
          ind = max(inds)
          if ind != -1:
              return all(i == ind for i in inds)
          else:
              return all([x == objs[0] for x in objs])
  
      def get_equiv_const(self, obj):
          """Check if obj is equivalent to some int constant, and return
          the constant if found, or None otherwise.
          """
          ind = self._get_ind(obj)
          if ind >= 0:
              objs = self.ind_to_obj[ind]
              for x in objs:
                  if isinstance(x, int):
                      return x
          return None
  
      def get_equiv_set(self, obj):
          """Return the set of equivalent objects.
          """
          ind = self._get_ind(obj)
          if ind >= 0:
              return set(self.ind_to_obj[ind])
          return set()
  
      def insert_equiv(self, *objs):
          """Insert a set of equivalent objects by modifying self. This
          method can be overloaded to transform object type before insertion.
          """
          return self._insert(objs)
  
      def intersect(self, equiv_set):
          """ Return the intersection of self and the given equiv_set,
          without modifying either of them. The result will also keep
          old equivalence indices unchanged.
          """
          new_set = self.empty()
          new_set.next_ind = self.next_ind
  
          for objs in equiv_set.ind_to_obj.values():
              inds = tuple(self._get_ind(x) for x in objs)
              ind_to_obj = {}
  
              for i, x in zip(inds, objs):
                  if i in ind_to_obj:
                      ind_to_obj[i].append(x)
                  elif i >= 0:
                      ind_to_obj[i] = [x]
  
              for v in ind_to_obj.values():
                  if len(v) > 1:
                      new_set._insert(v)
  
          return new_set
  
  
  class ShapeEquivSet(EquivSet):
  
      """Just like EquivSet, except that it accepts only numba IR variables
      and constants as objects, guided by their types. Arrays are considered
      equivalent as long as their shapes are equivalent. Scalars are
      equivalent only when they are equal in value. Tuples are equivalent
      when they are of the same size, and their elements are equivalent.
      """
  
      def __init__(
          self,
          typemap,
          defs=None,
          ind_to_var=None,
          obj_to_ind=None,
          ind_to_obj=None,
          next_id=0,
          ind_to_const=None,
      ):
          """Create a new ShapeEquivSet object, where typemap is a dictionary
          that maps variable names to their types, and it will not be modified.
          Optional keyword arguments are for internal use only.
          """
          self.typemap = typemap
          # defs maps variable name to an int, where
          # 1 means the variable is defined only once, and numbers greater
          # than 1 means defined more than onces.
          self.defs = defs if defs else {}
          # ind_to_var maps index number to a list of variables (of ir.Var type).
          # It is used to retrieve defined shape variables given an equivalence
          # index.
          self.ind_to_var = ind_to_var if ind_to_var else {}
          # ind_to_const maps index number to a constant, if known.
          self.ind_to_const = ind_to_const if ind_to_const else {}
  
          super(ShapeEquivSet, self).__init__(obj_to_ind, ind_to_obj, next_id)
  
      def empty(self):
          """Return an empty ShapeEquivSet.
          """
          return ShapeEquivSet(self.typemap, {})
  
      def clone(self):
          """Return a new copy.
          """
          return ShapeEquivSet(
              self.typemap,
              defs=copy.copy(self.defs),
              ind_to_var=copy.copy(self.ind_to_var),
              obj_to_ind=copy.deepcopy(self.obj_to_ind),
              ind_to_obj=copy.deepcopy(self.ind_to_obj),
              next_id=self.next_ind,
              ind_to_const=copy.deepcopy(self.ind_toconst),
          )
  
      def __repr__(self):
          return "ShapeEquivSet({}, ind_to_var={}, ind_to_const={})".format(
              self.ind_to_obj, self.ind_to_var, self.ind_to_const
          )
  
      def _get_names(self, obj):
          """Return a set of names for the given obj, where array and tuples
          are broken down to their individual shapes or elements. This is
          safe because both Numba array shapes and Python tuples are immutable.
          """
          if isinstance(obj, ir.Var) or isinstance(obj, str):
              name = obj if isinstance(obj, str) else obj.name
              if name not in self.typemap:
                  return (name,)
  
              typ = self.typemap[name]
              if isinstance(typ, (types.BaseTuple, types.ArrayCompatible)):
                  ndim = (typ.ndim
                          if isinstance(typ, types.ArrayCompatible)
                          else len(typ))
                  # Treat 0d array as if it were a scalar.
                  if ndim == 0:
                      return (name,)
                  else:
                      return tuple("{}#{}".format(name, i) for i in range(ndim))
              else:
                  return (name,)
          elif isinstance(obj, ir.Const):
              if isinstance(obj.value, tuple):
                  return obj.value
              else:
                  return (obj.value,)
          elif isinstance(obj, tuple):
  
              def get_names(x):
                  names = self._get_names(x)
                  if len(names) != 0:
                      return names[0]
                  return names
  
              return tuple(get_names(x) for x in obj)
          elif isinstance(obj, int):
              return (obj,)
          if config.DEBUG_ARRAY_OPT >= 1:
              print(
                  f"Ignoring untracked object type {type(obj)} in ShapeEquivSet")
          return ()
  
      def is_equiv(self, *objs):
          """Overload EquivSet.is_equiv to handle Numba IR variables and
          constants.
          """
          assert len(objs) > 1
          obj_names = [self._get_names(x) for x in objs]
          obj_names = [x for x in obj_names if x != ()]  # rule out 0d shape
          if len(obj_names) <= 1:
              return False
          ndims = [len(names) for names in obj_names]
          ndim = ndims[0]
          if not all(ndim == x for x in ndims):
              if config.DEBUG_ARRAY_OPT >= 1:
                  print("is_equiv: Dimension mismatch for {}".format(objs))
              return False
          for i in range(ndim):
              names = [obj_name[i] for obj_name in obj_names]
              if not super(ShapeEquivSet, self).is_equiv(*names):
                  return False
          return True
  
      def get_equiv_const(self, obj):
          """If the given object is equivalent to a constant scalar,
          return the scalar value, or None otherwise.
          """
          names = self._get_names(obj)
          if len(names) != 1:
              return None
          return super(ShapeEquivSet, self).get_equiv_const(names[0])
  
      def get_equiv_var(self, obj):
          """If the given object is equivalent to some defined variable,
          return the variable, or None otherwise.
          """
          names = self._get_names(obj)
          if len(names) != 1:
              return None
          ind = self._get_ind(names[0])
          vs = self.ind_to_var.get(ind, [])
          return vs[0] if vs != [] else None
  
      def get_equiv_set(self, obj):
          """Return the set of equivalent objects.
          """
          names = self._get_names(obj)
          if len(names) != 1:
              return None
          return super(ShapeEquivSet, self).get_equiv_set(names[0])
  
      def _insert(self, objs):
          """Overload EquivSet._insert to manage ind_to_var dictionary.
          """
          inds = []
          for obj in objs:
              if obj in self.obj_to_ind:
                  inds.append(self.obj_to_ind[obj])
          varlist = []
          constval = None
          names = set()
          for i in sorted(inds):
              if i in self.ind_to_var:
                  for x in self.ind_to_var[i]:
                      if not (x.name in names):
                          varlist.append(x)
                          names.add(x.name)
              if i in self.ind_to_const:
                  assert constval is None
                  constval = self.ind_to_const[i]
          super(ShapeEquivSet, self)._insert(objs)
          new_ind = self.obj_to_ind[objs[0]]
          for i in set(inds):
              if i in self.ind_to_var:
                  del self.ind_to_var[i]
          self.ind_to_var[new_ind] = varlist
          if constval is not None:
              self.ind_to_const[new_ind] = constval
  
      def insert_equiv(self, *objs):
          """Overload EquivSet.insert_equiv to handle Numba IR variables and
          constants. Input objs are either variable or constant, and at least
          one of them must be variable.
          """
          assert len(objs) > 1
          obj_names = [self._get_names(x) for x in objs]
          obj_names = [x for x in obj_names if x != ()]  # rule out 0d shape
          if len(obj_names) <= 1:
              return
          names = sum([list(x) for x in obj_names], [])
          ndims = [len(x) for x in obj_names]
          ndim = ndims[0]
          assert all(
              ndim == x for x in ndims
          ), "Dimension mismatch for {}".format(objs)
          varlist = []
          constlist = []
          for obj in objs:
              if not isinstance(obj, tuple):
                  obj = (obj,)
              for var in obj:
                  if isinstance(var, ir.Var) and not (var.name in varlist):
                      # favor those already defined, move to front of varlist
                      if var.name in self.defs:
                          varlist.insert(0, var)
                      else:
                          varlist.append(var)
                  if isinstance(var, ir.Const) and not (var.value in constlist):
                      constlist.append(var.value)
  
          # try to populate ind_to_var if variables are present
          for obj in varlist:
              name = obj.name
              if name in names and not (name in self.obj_to_ind):
                  self.ind_to_obj[self.next_ind] = [name]
                  self.obj_to_ind[name] = self.next_ind
                  self.ind_to_var[self.next_ind] = [obj]
                  self.next_ind += 1
  
          # create equivalence classes for previously unseen constants
          for const in constlist:
              if const in names and not (const in self.obj_to_ind):
                  self.ind_to_obj[self.next_ind] = [const]
                  self.obj_to_ind[const] = self.next_ind
                  self.ind_to_const[self.next_ind] = const
                  self.next_ind += 1
  
          some_change = False
  
          for i in range(ndim):
              names = [obj_name[i] for obj_name in obj_names]
              ie_res = super(ShapeEquivSet, self).insert_equiv(*names)
              some_change = some_change or ie_res
  
          return some_change
  
      def has_shape(self, name):
          """Return true if the shape of the given variable is available.
          """
          return self.get_shape(name) is not None
  
      def get_shape(self, name):
          """Return a tuple of variables that corresponds to the shape
          of the given array, or None if not found.
          """
          return guard(self._get_shape, name)
  
      def _get_shape(self, name):
          """Return a tuple of variables that corresponds to the shape
          of the given array, or raise GuardException if not found.
          """
          inds = self.get_shape_classes(name)
          require(inds != ())
          shape = []
          for i in inds:
              require(i in self.ind_to_var)
              vs = self.ind_to_var[i]
              if vs != []:
                  shape.append(vs[0])
              else:
                  require(i in self.ind_to_const)
                  vs = self.ind_to_const[i]
                  shape.append(vs)
          return tuple(shape)
  
      def get_shape_classes(self, name):
          """Instead of the shape tuple, return tuple of int, where
          each int is the corresponding class index of the size object.
          Unknown shapes are given class index -1. Return empty tuple
          if the input name is a scalar variable.
          """
          if isinstance(name, ir.Var):
              name = name.name
          typ = self.typemap[name] if name in self.typemap else None
          if not (
              isinstance(typ, (
                  types.BaseTuple, types.SliceType, types.ArrayCompatible
              ))
          ):
              return []
          # Treat 0d arrays like scalars.
          if isinstance(typ, types.ArrayCompatible) and typ.ndim == 0:
              return []
          names = self._get_names(name)
          inds = tuple(self._get_ind(name) for name in names)
          return inds
  
      def intersect(self, equiv_set):
          """Overload the intersect method to handle ind_to_var.
          """
          newset = super(ShapeEquivSet, self).intersect(equiv_set)
          ind_to_var = {}
          for i, objs in newset.ind_to_obj.items():
              assert len(objs) > 0
              obj = objs[0]
              assert obj in self.obj_to_ind
              assert obj in equiv_set.obj_to_ind
              j = self.obj_to_ind[obj]
              k = equiv_set.obj_to_ind[obj]
              assert j in self.ind_to_var
              assert k in equiv_set.ind_to_var
              varlist = []
              names = [x.name for x in equiv_set.ind_to_var[k]]
              for x in self.ind_to_var[j]:
                  if x.name in names:
                      varlist.append(x)
              ind_to_var[i] = varlist
          newset.ind_to_var = ind_to_var
          return newset
  
      def define(self, name, redefined):
          """Increment the internal count of how many times a variable is being
          defined. Most variables in Numba IR are SSA, i.e., defined only once,
          but not all of them. When a variable is being re-defined, it must
          be removed from the equivalence relation and added to the redefined
          set but only if that redefinition is not known to have the same
          equivalence classes. Those variables redefined are removed from all
          the blocks' equivalence sets later.
  
          Arrays passed to define() use their whole name but these do not
          appear in the equivalence sets since they are stored there per
          dimension. Calling _get_names() here converts array names to
          dimensional names.
  
          This function would previously invalidate if there were any multiple
          definitions of a variable.  However, we realized that this behavior
          is overly restrictive.  You need only invalidate on multiple
          definitions if they are not known to be equivalent. So, the
          equivalence insertion functions now return True if some change was
          made (meaning the definition was not equivalent) and False
          otherwise. If no change was made, then define() need not be
          called. For no change to have been made, the variable must
          already be present. If the new definition of the var has the
          case where lhs and rhs are in the same equivalence class then
          again, no change will be made and define() need not be called
          or the variable invalidated.
          """
          if isinstance(name, ir.Var):
              name = name.name
          if name in self.defs:
              self.defs[name] += 1
              name_res = list(self._get_names(name))
              for one_name in name_res:
                  # NOTE: variable being redefined, must invalidate previous
                  # equivalences. Believe it is a rare case, and only happens to
                  # scalar accumuators.
                  if one_name in self.obj_to_ind:
                      redefined.add(
                          one_name
                      )  # remove this var from all equiv sets
                      i = self.obj_to_ind[one_name]
                      del self.obj_to_ind[one_name]
                      self.ind_to_obj[i].remove(one_name)
                      if self.ind_to_obj[i] == []:
                          del self.ind_to_obj[i]
                      assert i in self.ind_to_var
                      names = [x.name for x in self.ind_to_var[i]]
                      if name in names:
                          j = names.index(name)
                          del self.ind_to_var[i][j]
                          if self.ind_to_var[i] == []:
                              del self.ind_to_var[i]
                              # no more size variables, remove equivalence too
                              if i in self.ind_to_obj:
                                  for obj in self.ind_to_obj[i]:
                                      del self.obj_to_ind[obj]
                                  del self.ind_to_obj[i]
          else:
              self.defs[name] = 1
  
      def union_defs(self, defs, redefined):
          """Union with the given defs dictionary. This is meant to handle
          branch join-point, where a variable may have been defined in more
          than one branches.
          """
          for k, v in defs.items():
              if v > 0:
                  self.define(k, redefined)
  
  
  class SymbolicEquivSet(ShapeEquivSet):
  
      """Just like ShapeEquivSet, except that it also reasons about variable
      equivalence symbolically by using their arithmetic definitions.
      The goal is to automatically derive the equivalence of array ranges
      (slicing). For instance, a[1:m] and a[0:m-1] shall be considered
      size-equivalence.
      """
  
      def __init__(
          self,
          typemap,
          def_by=None,
          ref_by=None,
          ext_shapes=None,
          defs=None,
          ind_to_var=None,
          obj_to_ind=None,
          ind_to_obj=None,
          next_id=0,
      ):
          """Create a new SymbolicEquivSet object, where typemap is a dictionary
          that maps variable names to their types, and it will not be modified.
          Optional keyword arguments are for internal use only.
          """
          # A "defined-by" table that maps A to a tuple of (B, i), which
          # means A is defined as: A = B + i, where A,B are variable names,
          # and i is an integer constants.
          self.def_by = def_by if def_by else {}
          # A "refered-by" table that maps A to a list of [(B, i), (C, j) ...],
          # which implies a sequence of definitions: B = A - i, C = A - j, and
          # so on, where A,B,C,... are variable names, and i,j,... are
          # integer constants.
          self.ref_by = ref_by if ref_by else {}
          # A extended shape table that can map an arbitrary object to a shape,
          # currently used to remember shapes for SetItem IR node, and wrapped
          # indices for Slice objects.
          self.ext_shapes = ext_shapes if ext_shapes else {}
          # rel_map keeps a map of relative sizes that we have seen so
          # that if we compute the same relative sizes different times
          # in different ways we can associate those two instances
          # of the same relative size to the same equivalence class.
          self.rel_map = {}
          # wrap_index() computes the effectual index given a slice and a
          # dimension's size.  We need to be able to know that two wrap_index
          # calls are equivalent.  They are known to be equivalent if the slice
          # and dimension sizes of the two wrap_index calls are equivalent.
          # wrap_map maps from a tuple of equivalence class ids for a slice and
          # a dimension size to some new equivalence class id for the output size.
          self.wrap_map = {}
          super(SymbolicEquivSet, self).__init__(
              typemap, defs, ind_to_var, obj_to_ind, ind_to_obj, next_id
          )
  
      def empty(self):
          """Return an empty SymbolicEquivSet.
          """
          return SymbolicEquivSet(self.typemap)
  
      def __repr__(self):
          return (
              "SymbolicEquivSet({}, ind_to_var={}, def_by={}, "
              "ref_by={}, ext_shapes={})".format(
                  self.ind_to_obj,
                  self.ind_to_var,
                  self.def_by,
                  self.ref_by,
                  self.ext_shapes,
              )
          )
  
      def clone(self):
          """Return a new copy.
          """
          return SymbolicEquivSet(
              self.typemap,
              def_by=copy.copy(self.def_by),
              ref_by=copy.copy(self.ref_by),
              ext_shapes=copy.copy(self.ext_shapes),
              defs=copy.copy(self.defs),
              ind_to_var=copy.copy(self.ind_to_var),
              obj_to_ind=copy.deepcopy(self.obj_to_ind),
              ind_to_obj=copy.deepcopy(self.ind_to_obj),
              next_id=self.next_ind,
          )
  
      def get_rel(self, name):
          """Retrieve a definition pair for the given variable,
          or return None if it is not available.
          """
          return guard(self._get_or_set_rel, name)
  
      def _get_or_set_rel(self, name, func_ir=None):
          """Retrieve a definition pair for the given variable,
          and if it is not already available, try to look it up
          in the given func_ir, and remember it for future use.
          """
          if isinstance(name, ir.Var):
              name = name.name
          require(self.defs.get(name, 0) == 1)
          if name in self.def_by:
              return self.def_by[name]
          else:
              require(func_ir is not None)
  
              def plus(x, y):
                  x_is_const = isinstance(x, int)
                  y_is_const = isinstance(y, int)
                  if x_is_const:
                      if y_is_const:
                          return x + y
                      else:
                          (var, offset) = y
                          return (var, x + offset)
                  else:
                      (var, offset) = x
                      if y_is_const:
                          return (var, y + offset)
                      else:
                          return None
  
              def minus(x, y):
                  if isinstance(y, int):
                      return plus(x, -y)
                  elif (
                      isinstance(x, tuple)
                      and isinstance(y, tuple)
                      and x[0] == y[0]
                  ):
                      return minus(x[1], y[1])
                  else:
                      return None
  
              expr = get_definition(func_ir, name)
              value = (name, 0)  # default to its own name
              if isinstance(expr, ir.Expr):
                  if expr.op == "call":
                      fname, mod_name = find_callname(
                          func_ir, expr, typemap=self.typemap
                      )
                      if (
                          fname == "wrap_index"
                          and mod_name == "numba.parfors.array_analysis"
                      ):
                          index = tuple(
                              self.obj_to_ind.get(x.name, -1) for x in expr.args
                          )
                          # If wrap_index for a slice works on a variable
                          # that is not analyzable (e.g., multiple definitions)
                          # then we have to return None here since we can't know
                          # how that size will compare to others if we can't
                          # analyze some part of the slice.
                          if -1 in index:
                              return None
                          names = self.ext_shapes.get(index, [])
                          names.append(name)
                          if len(names) > 0:
                              self._insert(names)
                          self.ext_shapes[index] = names
                  elif expr.op == "binop":
                      lhs = self._get_or_set_rel(expr.lhs, func_ir)
                      rhs = self._get_or_set_rel(expr.rhs, func_ir)
                      # If either the lhs or rhs is not analyzable
                      # then don't try to record information this var.
                      if lhs is None or rhs is None:
                          return None
                      elif expr.fn == operator.add:
                          value = plus(lhs, rhs)
                      elif expr.fn == operator.sub:
                          value = minus(lhs, rhs)
              elif isinstance(expr, ir.Const) and isinstance(expr.value, int):
                  value = expr.value
              require(value is not None)
              # update def_by table
              self.def_by[name] = value
              if isinstance(value, int) or (
                  isinstance(value, tuple)
                  and (value[0] != name or value[1] != 0)
              ):
                  # update ref_by table too
                  if isinstance(value, tuple):
                      (var, offset) = value
                      if not (var in self.ref_by):
                          self.ref_by[var] = []
                      self.ref_by[var].append((name, -offset))
                      # insert new equivalence if found
                      ind = self._get_ind(var)
                      if ind >= 0:
                          objs = self.ind_to_obj[ind]
                          names = []
                          for obj in objs:
                              if obj in self.ref_by:
                                  names += [
                                      x
                                      for (x, i) in self.ref_by[obj]
                                      if i == -offset
                                  ]
                          if len(names) > 1:
                              super(SymbolicEquivSet, self)._insert(names)
              return value
  
      def define(self, var, redefined, func_ir=None, typ=None):
          """Besides incrementing the definition count of the given variable
          name, it will also retrieve and simplify its definition from func_ir,
          and remember the result for later equivalence comparison. Supported
          operations are:
            1. arithmetic plus and minus with constants
            2. wrap_index (relative to some given size)
          """
          if isinstance(var, ir.Var):
              name = var.name
          else:
              name = var
          super(SymbolicEquivSet, self).define(name, redefined)
          if (
              func_ir
              and self.defs.get(name, 0) == 1
              and isinstance(typ, types.Number)
          ):
              value = guard(self._get_or_set_rel, name, func_ir)
              # turn constant definition into equivalence
              if isinstance(value, int):
                  self._insert([name, value])
              if isinstance(var, ir.Var):
                  ind = self._get_or_add_ind(name)
                  if not (ind in self.ind_to_obj):
                      self.ind_to_obj[ind] = [name]
                      self.obj_to_ind[name] = ind
                  if ind in self.ind_to_var:
                      self.ind_to_var[ind].append(var)
                  else:
                      self.ind_to_var[ind] = [var]
              return True
  
      def _insert(self, objs):
          """Overload _insert method to handle ind changes between relative
          objects.  Returns True if some change is made, false otherwise.
          """
          indset = set()
          uniqs = set()
          for obj in objs:
              ind = self._get_ind(obj)
              if ind == -1:
                  uniqs.add(obj)
              elif not (ind in indset):
                  uniqs.add(obj)
                  indset.add(ind)
          if len(uniqs) <= 1:
              return False
          uniqs = list(uniqs)
          super(SymbolicEquivSet, self)._insert(uniqs)
          objs = self.ind_to_obj[self._get_ind(uniqs[0])]
  
          # New equivalence guided by def_by and ref_by
          offset_dict = {}
  
          def get_or_set(d, k):
              if k in d:
                  v = d[k]
              else:
                  v = []
                  d[k] = v
              return v
  
          for obj in objs:
              if obj in self.def_by:
                  value = self.def_by[obj]
                  if isinstance(value, tuple):
                      (name, offset) = value
                      get_or_set(offset_dict, -offset).append(name)
                      if name in self.ref_by:  # relative to name
                          for (v, i) in self.ref_by[name]:
                              get_or_set(offset_dict, -(offset + i)).append(v)
              if obj in self.ref_by:
                  for (name, offset) in self.ref_by[obj]:
                      get_or_set(offset_dict, offset).append(name)
          for names in offset_dict.values():
              self._insert(names)
          return True
  
      def set_shape_setitem(self, obj, shape):
          """remember shapes of SetItem IR nodes.
          """
          assert isinstance(obj, (ir.StaticSetItem, ir.SetItem))
          self.ext_shapes[obj] = shape
  
      def _get_shape(self, obj):
          """Overload _get_shape to retrieve the shape of SetItem IR nodes.
          """
          if isinstance(obj, (ir.StaticSetItem, ir.SetItem)):
              require(obj in self.ext_shapes)
              return self.ext_shapes[obj]
          else:
              assert isinstance(obj, ir.Var)
              typ = self.typemap[obj.name]
              # for slice type, return the shape variable itself
              if isinstance(typ, types.SliceType):
                  return (obj,)
              else:
                  return super(SymbolicEquivSet, self)._get_shape(obj)
  
  
  class WrapIndexMeta(object):
      """
        Array analysis should be able to analyze all the function
        calls that it adds to the IR.  That way, array analysis can
        be run as often as needed and you should get the same
        equivalencies.  One modification to the IR that array analysis
        makes is the insertion of wrap_index calls.  Thus, repeated
        array analysis passes should be able to analyze these wrap_index
        calls.  The difficulty of these calls is that the equivalence
        class of the left-hand side of the assignment is not present in
        the arguments to wrap_index in the right-hand side.  Instead,
        the equivalence class of the wrap_index output is a combination
        of the wrap_index args.  The important thing to
        note is that if the equivalence classes of the slice size
        and the dimension's size are the same for two wrap index
        calls then we can be assured of the answer being the same.
        So, we maintain the wrap_map dict that maps from a tuple
        of equivalence class ids for the slice and dimension size
        to some new equivalence class id for the output size.
        However, when we are analyzing the first such wrap_index
        call we don't have a variable there to associate to the
        size since we're in the process of analyzing the instruction
        that creates that mapping.  So, instead we return an object
        of this special class and analyze_inst will establish the
        connection between a tuple of the parts of this object
        below and the left-hand side variable.
      """
  
      def __init__(self, slice_size, dim_size):
          self.slice_size = slice_size
          self.dim_size = dim_size
  
  
  @register_jitable
  def runtime_broadcast_assert_shapes(max_dim, arg0, arg1):
      new_shape = []
      shapes = []
      shapes.append(list(arg0.shape))
      shapes.append(list(arg1.shape))
      for i in range(max_dim):
          sizes = []
          for shape in shapes:
              if i < len(shape):
                  size = shape[len(shape) - 1 - i]
                  if size != 1:
                      sizes.append(size)  # non-1 size to front
          if len(sizes) == 0:
              sizes.append(1)
          new_shape.append(sizes[0])
  
      rev = new_shape[::-1]
      return to_fixed_tuple(rev, max_dim)
  
  
  class ArrayAnalysis(object):
      aa_count = 0
  
      """Analyzes Numpy array computations for properties such as
      shape/size equivalence, and keeps track of them on a per-block
      basis. The analysis should only be run once because it modifies
      the incoming IR by inserting assertion statements that safeguard
      parfor optimizations.
      """
  
      def __init__(self, context, func_ir, typemap, calltypes):
          self.context = context
          self.func_ir = func_ir
          self.typemap = typemap
          self.calltypes = calltypes
  
          # EquivSet of variables, indexed by block number
          self.equiv_sets = {}
          # keep attr calls to arrays like t=A.sum() as {t:('sum',A)}
          self.array_attr_calls = {}
          # keep attrs of objects (value,attr)->shape_var
          self.object_attrs = {}
          # keep prepended instructions from conditional branch
          self.prepends = {}
          # keep track of pruned precessors when branch degenerates to jump
          self.pruned_predecessors = {}
  
      def get_equiv_set(self, block_label):
          """Return the equiv_set object of an block given its label.
          """
          return self.equiv_sets[block_label]
  
      def remove_redefineds(self, redefineds):
          """Take a set of variables in redefineds and go through all
          the currently existing equivalence sets (created in topo order)
          and remove that variable from all of them since it is multiply
          defined within the function.
          """
          unused = set()
          for r in redefineds:
              for eslabel in self.equiv_sets:
                  es = self.equiv_sets[eslabel]
                  es.define(r, unused)
  
      def run(self, blocks=None, equiv_set=None):
          """run array shape analysis on the given IR blocks, resulting in
          modified IR and finalized EquivSet for each block.
          """
          if blocks is None:
              blocks = self.func_ir.blocks
  
          self.func_ir._definitions = build_definitions(self.func_ir.blocks)
  
          if equiv_set is None:
              init_equiv_set = SymbolicEquivSet(self.typemap)
          else:
              init_equiv_set = equiv_set
  
          self.alias_map, self.arg_aliases = find_potential_aliases(
              blocks,
              self.func_ir.arg_names,
              self.typemap,
              self.func_ir
          )
  
          aa_count_save = ArrayAnalysis.aa_count
          ArrayAnalysis.aa_count += 1
          if config.DEBUG_ARRAY_OPT >= 1:
              print("Starting ArrayAnalysis:", aa_count_save)
          dprint_func_ir(self.func_ir, "before array analysis", blocks)
  
          if config.DEBUG_ARRAY_OPT >= 1:
              print(
                  "ArrayAnalysis variable types: ", sorted(self.typemap.items())
              )
              print("ArrayAnalysis call types: ", self.calltypes)
  
          cfg = compute_cfg_from_blocks(blocks)
          topo_order = find_topo_order(blocks, cfg=cfg)
          # Traverse blocks in topological order
          self._run_on_blocks(topo_order, blocks, cfg, init_equiv_set)
  
          if config.DEBUG_ARRAY_OPT >= 1:
              self.dump()
              print(
                  "ArrayAnalysis post variable types: ",
                  sorted(self.typemap.items()),
              )
              print("ArrayAnalysis post call types: ", self.calltypes)
  
          dprint_func_ir(self.func_ir, "after array analysis", blocks)
          if config.DEBUG_ARRAY_OPT >= 1:
              print("Ending ArrayAnalysis:", aa_count_save)
  
      def _run_on_blocks(self, topo_order, blocks, cfg, init_equiv_set):
          for label in topo_order:
              if config.DEBUG_ARRAY_OPT >= 2:
                  print("Processing block:", label)
              block = blocks[label]
              scope = block.scope
              pending_transforms = self._determine_transform(
                  cfg, block, label, scope, init_equiv_set
              )
              self._combine_to_new_block(block, pending_transforms)
  
      def _combine_to_new_block(self, block, pending_transforms):
          """Combine the new instructions from previous pass into a new block
          body.
          """
          new_body = []
          for inst, pre, post in pending_transforms:
              for instr in pre:
                  new_body.append(instr)
              new_body.append(inst)
              for instr in post:
                  new_body.append(instr)
          block.body = new_body
  
      def _determine_transform(self, cfg, block, label, scope, init_equiv_set):
          """Determine the transformation for each instruction in the block
          """
          equiv_set = None
          # equiv_set is the intersection of predecessors
          preds = cfg.predecessors(label)
          # some incoming edge may be pruned due to prior analysis
          if label in self.pruned_predecessors:
              pruned = self.pruned_predecessors[label]
          else:
              pruned = []
          # Go through each incoming edge, process prepended instructions and
          # calculate beginning equiv_set of current block as an intersection
          # of incoming ones.
          if config.DEBUG_ARRAY_OPT >= 2:
              print("preds:", preds)
          for (p, q) in preds:
              if config.DEBUG_ARRAY_OPT >= 2:
                  print("p, q:", p, q)
              if p in pruned:
                  continue
              if p in self.equiv_sets:
                  from_set = self.equiv_sets[p].clone()
                  if config.DEBUG_ARRAY_OPT >= 2:
                      print("p in equiv_sets", from_set)
                  if (p, label) in self.prepends:
                      instrs = self.prepends[(p, label)]
                      for inst in instrs:
                          redefined = set()
                          self._analyze_inst(
                              label, scope, from_set, inst, redefined
                          )
                          # Remove anything multiply defined in this block
                          # from every block equivs.
                          # NOTE: necessary? can't observe effect in testsuite
                          self.remove_redefineds(redefined)
                  if equiv_set is None:
                      equiv_set = from_set
                  else:
                      equiv_set = equiv_set.intersect(from_set)
                      redefined = set()
                      equiv_set.union_defs(from_set.defs, redefined)
                      # Remove anything multiply defined in this block
                      # from every block equivs.
                      # NOTE: necessary? can't observe effect in testsuite
                      self.remove_redefineds(redefined)
  
          # Start with a new equiv_set if none is computed
          if equiv_set is None:
              equiv_set = init_equiv_set
          self.equiv_sets[label] = equiv_set
  
          # Go through instructions in a block, and insert pre/post
          # instructions as we analyze them.
          pending_transforms = []
          for inst in block.body:
              redefined = set()
              pre, post = self._analyze_inst(
                  label, scope, equiv_set, inst, redefined
              )
              # Remove anything multiply defined in this block from every block
              # equivs.
              if len(redefined) > 0:
                  self.remove_redefineds(redefined)
  
              pending_transforms.append((inst, pre, post))
          return pending_transforms
  
      def dump(self):
          """dump per-block equivalence sets for debugging purposes.
          """
          print("Array Analysis: ", self.equiv_sets)
  
      def _define(self, equiv_set, var, typ, value):
          self.typemap[var.name] = typ
          self.func_ir._definitions[var.name] = [value]
          redefineds = set()
          equiv_set.define(var, redefineds, self.func_ir, typ)
  
      class AnalyzeResult(object):
          def __init__(self, **kwargs):
              self.kwargs = kwargs
  
      def _analyze_inst(self, label, scope, equiv_set, inst, redefined):
          pre = []
          post = []
          if config.DEBUG_ARRAY_OPT >= 2:
              print("analyze_inst:", inst)
          if isinstance(inst, ir.Assign):
              lhs = inst.target
              typ = self.typemap[lhs.name]
              shape = None
              if isinstance(typ, types.ArrayCompatible) and typ.ndim == 0:
                  shape = ()
              elif isinstance(inst.value, ir.Expr):
                  result = self._analyze_expr(scope, equiv_set, inst.value, lhs)
                  if result:
                      require(isinstance(result, ArrayAnalysis.AnalyzeResult))
                      if 'shape' in result.kwargs:
                          shape = result.kwargs['shape']
                      if 'pre' in result.kwargs:
                          pre.extend(result.kwargs['pre'])
                      if 'post' in result.kwargs:
                          post.extend(result.kwargs['post'])
                      if 'rhs' in result.kwargs:
                          inst.value = result.kwargs['rhs']
              elif isinstance(inst.value, (ir.Var, ir.Const)):
                  shape = inst.value
              elif isinstance(inst.value, ir.Global):
                  gvalue = inst.value.value
                  # only integer values can be part of shape
                  # TODO: support cases with some but not all integer values or
                  # nested tuples
                  if (isinstance(gvalue, tuple)
                          and all(isinstance(v, int) for v in gvalue)):
                      shape = gvalue
                  elif isinstance(gvalue, int):
                      shape = (gvalue,)
              elif isinstance(inst.value, ir.Arg):
                  if (
                      isinstance(typ, types.containers.UniTuple)
                      and isinstance(typ.dtype, types.Integer)
                  ):
                      shape = inst.value
                  elif (
                      isinstance(typ, types.containers.Tuple)
                      and all([isinstance(x,
                              (types.Integer, types.IntegerLiteral))
                          for x in typ.types]
                      )
                  ):
                      shape = inst.value
  
              if isinstance(shape, ir.Const):
                  if isinstance(shape.value, tuple):
                      loc = shape.loc
                      shape = tuple(ir.Const(x, loc) for x in shape.value)
                  elif isinstance(shape.value, int):
                      shape = (shape,)
                  else:
                      shape = None
              elif isinstance(shape, ir.Var) and isinstance(
                  self.typemap[shape.name], types.Integer
              ):
                  shape = (shape,)
              elif isinstance(shape, WrapIndexMeta):
                  """ Here we've got the special WrapIndexMeta object
                      back from analyzing a wrap_index call.  We define
                      the lhs and then get it's equivalence class then
                      add the mapping from the tuple of slice size and
                      dimensional size equivalence ids to the lhs
                      equivalence id.
                  """
                  equiv_set.define(lhs, redefined, self.func_ir, typ)
                  lhs_ind = equiv_set._get_ind(lhs.name)
                  if lhs_ind != -1:
                      equiv_set.wrap_map[
                          (shape.slice_size, shape.dim_size)
                      ] = lhs_ind
                  return pre, post
  
              if isinstance(typ, types.ArrayCompatible):
                  if (
                      shape is not None
                      and isinstance(shape, ir.Var)
                      and isinstance(
                          self.typemap[shape.name], types.containers.Tuple
                      )
                  ):
                      pass
                  elif (
                      shape is None
                      or isinstance(shape, tuple)
                      or (
                          isinstance(shape, ir.Var)
                          and not equiv_set.has_shape(shape)
                      )
                  ):
                      shape = self._gen_shape_call(
                          equiv_set, lhs, typ.ndim, shape, post
                      )
              elif isinstance(typ, types.UniTuple):
                  if shape and isinstance(typ.dtype, types.Integer):
                      shape = self._gen_shape_call(
                          equiv_set, lhs, len(typ), shape, post
                      )
              elif (
                  isinstance(typ, types.containers.Tuple)
                  and all([isinstance(x,
                          (types.Integer, types.IntegerLiteral))
                      for x in typ.types]
                  )
              ):
                  shape = self._gen_shape_call(
                      equiv_set, lhs, len(typ), shape, post
                  )
  
              """ See the comment on the define() function.
  
                  We need only call define(), which will invalidate a variable
                  from being in the equivalence sets on multiple definitions,
                  if the variable was not previously defined or if the new
                  definition would be in a conflicting equivalence class to the
                  original equivalence class for the variable.
  
                  insert_equiv() returns True if either of these conditions are
                  True and then we call define() in those cases.
                  If insert_equiv() returns False then no changes were made and
                  all equivalence classes are consistent upon a redefinition so
                  no invalidation is needed and we don't call define().
              """
              needs_define = True
              if shape is not None:
                  needs_define = equiv_set.insert_equiv(lhs, shape)
              if needs_define:
                  equiv_set.define(lhs, redefined, self.func_ir, typ)
          elif isinstance(inst, (ir.StaticSetItem, ir.SetItem)):
              index = (
                  inst.index if isinstance(inst, ir.SetItem) else inst.index_var
              )
              result = guard(
                  self._index_to_shape, scope, equiv_set, inst.target, index
              )
              if not result:
                  return [], []
              if result[0] is not None:
                  assert isinstance(inst, (ir.StaticSetItem, ir.SetItem))
                  inst.index = result[0]
              result = result[1]
              target_shape = result.kwargs['shape']
              if 'pre' in result.kwargs:
                  pre = result.kwargs['pre']
              value_shape = equiv_set.get_shape(inst.value)
              if value_shape == ():  # constant
                  equiv_set.set_shape_setitem(inst, target_shape)
                  return pre, []
              elif value_shape is not None:
                  target_typ = self.typemap[inst.target.name]
                  require(isinstance(target_typ, types.ArrayCompatible))
                  target_ndim = target_typ.ndim
                  shapes = [target_shape, value_shape]
                  names = [inst.target.name, inst.value.name]
                  broadcast_result = self._broadcast_assert_shapes(
                      scope, equiv_set, inst.loc, shapes, names
                  )
                  require('shape' in broadcast_result.kwargs)
                  require('pre' in broadcast_result.kwargs)
                  shape = broadcast_result.kwargs['shape']
                  asserts = broadcast_result.kwargs['pre']
                  n = len(shape)
                  # shape dimension must be within target dimension
                  assert target_ndim >= n
                  equiv_set.set_shape_setitem(inst, shape)
                  return pre + asserts, []
              else:
                  return pre, []
          elif isinstance(inst, ir.Branch):
  
              def handle_call_binop(cond_def):
                  br = None
                  if cond_def.fn == operator.eq:
                      br = inst.truebr
                      otherbr = inst.falsebr
                      cond_val = 1
                  elif cond_def.fn == operator.ne:
                      br = inst.falsebr
                      otherbr = inst.truebr
                      cond_val = 0
                  lhs_typ = self.typemap[cond_def.lhs.name]
                  rhs_typ = self.typemap[cond_def.rhs.name]
                  if br is not None and (
                      (
                          isinstance(lhs_typ, types.Integer)
                          and isinstance(rhs_typ, types.Integer)
                      )
                      or (
                          isinstance(lhs_typ, types.BaseTuple)
                          and isinstance(rhs_typ, types.BaseTuple)
                      )
                  ):
                      loc = inst.loc
                      args = (cond_def.lhs, cond_def.rhs)
                      asserts = self._make_assert_equiv(
                          scope, loc, equiv_set, args
                      )
                      asserts.append(
                          ir.Assign(ir.Const(cond_val, loc), cond_var, loc)
                      )
                      self.prepends[(label, br)] = asserts
                      self.prepends[(label, otherbr)] = [
                          ir.Assign(ir.Const(1 - cond_val, loc), cond_var, loc)
                      ]
  
              cond_var = inst.cond
              cond_def = guard(get_definition, self.func_ir, cond_var)
              if not cond_def:  # phi variable has no single definition
                  # We'll use equiv_set to try to find a cond_def instead
                  equivs = equiv_set.get_equiv_set(cond_var)
                  defs = []
                  for name in equivs:
                      if isinstance(name, str) and name in self.typemap:
                          var_def = guard(
                              get_definition, self.func_ir, name, lhs_only=True
                          )
                          if isinstance(var_def, ir.Var):
                              var_def = var_def.name
                          if var_def:
                              defs.append(var_def)
                      else:
                          defs.append(name)
                  defvars = set(filter(lambda x: isinstance(x, str), defs))
                  defconsts = set(defs).difference(defvars)
                  if len(defconsts) == 1:
                      cond_def = list(defconsts)[0]
                  elif len(defvars) == 1:
                      cond_def = guard(
                          get_definition, self.func_ir, list(defvars)[0]
                      )
              if isinstance(cond_def, ir.Expr) and cond_def.op == 'binop':
                  handle_call_binop(cond_def)
              elif isinstance(cond_def, ir.Expr) and cond_def.op == 'call':
                  # this handles bool(predicate)
                  glbl_bool = guard(get_definition, self.func_ir, cond_def.func)
                  if glbl_bool is not None and glbl_bool.value is bool:
                      if len(cond_def.args) == 1:
                          condition = guard(get_definition, self.func_ir,
                                            cond_def.args[0])
                          if (condition is not None and
                              isinstance(condition, ir.Expr) and
                                  condition.op == 'binop'):
                              handle_call_binop(condition)
              else:
                  if isinstance(cond_def, ir.Const):
                      cond_def = cond_def.value
                  if isinstance(cond_def, int) or isinstance(cond_def, bool):
                      # condition is always true/false, prune the outgoing edge
                      pruned_br = inst.falsebr if cond_def else inst.truebr
                      if pruned_br in self.pruned_predecessors:
                          self.pruned_predecessors[pruned_br].append(label)
                      else:
                          self.pruned_predecessors[pruned_br] = [label]
  
          elif type(inst) in array_analysis_extensions:
              # let external calls handle stmt if type matches
              f = array_analysis_extensions[type(inst)]
              pre, post = f(inst, equiv_set, self.typemap, self)
  
          return pre, post
  
      def _analyze_expr(self, scope, equiv_set, expr, lhs):
          fname = "_analyze_op_{}".format(expr.op)
          try:
              fn = getattr(self, fname)
          except AttributeError:
              return None
          return guard(fn, scope, equiv_set, expr, lhs)
  
      def _analyze_op_getattr(self, scope, equiv_set, expr, lhs):
          # TODO: getattr of npytypes.Record
          if expr.attr == "T" and self._isarray(expr.value.name):
              return self._analyze_op_call_numpy_transpose(
                  scope, equiv_set, expr.loc, [expr.value], {}
              )
          elif expr.attr == "shape":
              shape = equiv_set.get_shape(expr.value)
              return ArrayAnalysis.AnalyzeResult(shape=shape)
          elif self._isarray(lhs.name):
              canonical_value = get_canonical_alias(
                  expr.value.name, self.alias_map
              )
              if (canonical_value, expr.attr) in self.object_attrs:
                  return ArrayAnalysis.AnalyzeResult(
                      shape=self.object_attrs[(canonical_value, expr.attr)]
                  )
              else:
                  typ = self.typemap[lhs.name]
                  post = []
                  shape = self._gen_shape_call(
                      equiv_set, lhs, typ.ndim, None, post
                  )
                  self.object_attrs[(canonical_value, expr.attr)] = shape
                  return ArrayAnalysis.AnalyzeResult(shape=shape, post=post)
  
          return None
  
      def _analyze_op_cast(self, scope, equiv_set, expr, lhs):
          return ArrayAnalysis.AnalyzeResult(shape=expr.value)
  
      def _analyze_op_exhaust_iter(self, scope, equiv_set, expr, lhs):
          var = expr.value
          typ = self.typemap[var.name]
          if isinstance(typ, types.BaseTuple):
              require(len(typ) == expr.count)
              require(equiv_set.has_shape(var))
              return ArrayAnalysis.AnalyzeResult(shape=var)
          return None
  
      def gen_literal_slice_part(
          self,
          arg_val,
          loc,
          scope,
          stmts,
          equiv_set,
          name="static_literal_slice_part",
      ):
          # Create var to hold the calculated slice size.
          static_literal_slice_part_var = ir.Var(scope, mk_unique_var(name), loc)
          static_literal_slice_part_val = ir.Const(arg_val, loc)
          static_literal_slice_part_typ = types.IntegerLiteral(arg_val)
          # We'll prepend this slice size calculation to the get/setitem.
          stmts.append(
              ir.Assign(
                  value=static_literal_slice_part_val,
                  target=static_literal_slice_part_var,
                  loc=loc,
              )
          )
          self._define(
              equiv_set,
              static_literal_slice_part_var,
              static_literal_slice_part_typ,
              static_literal_slice_part_val,
          )
          return static_literal_slice_part_var, static_literal_slice_part_typ
  
      def gen_static_slice_size(
          self, lhs_rel, rhs_rel, loc, scope, stmts, equiv_set
      ):
          the_var, *_ = self.gen_literal_slice_part(
              rhs_rel - lhs_rel,
              loc,
              scope,
              stmts,
              equiv_set,
              name="static_slice_size",
          )
          return the_var
  
      def gen_explicit_neg(
          self,
          arg,
          arg_rel,
          arg_typ,
          size_typ,
          loc,
          scope,
          dsize,
          stmts,
          equiv_set,
      ):
          assert not isinstance(size_typ, int)
          # Create var to hold the calculated slice size.
          explicit_neg_var = ir.Var(scope, mk_unique_var("explicit_neg"), loc)
          explicit_neg_val = ir.Expr.binop(operator.add, dsize, arg, loc=loc)
          # Determine the type of that var.  Can be literal if we know the
          # literal size of the dimension.
          explicit_neg_typ = types.intp
          self.calltypes[explicit_neg_val] = signature(
              explicit_neg_typ, size_typ, arg_typ
          )
          # We'll prepend this slice size calculation to the get/setitem.
          stmts.append(
              ir.Assign(value=explicit_neg_val, target=explicit_neg_var, loc=loc)
          )
          self._define(
              equiv_set, explicit_neg_var, explicit_neg_typ, explicit_neg_val
          )
          return explicit_neg_var, explicit_neg_typ
  
      def update_replacement_slice(
          self,
          lhs,
          lhs_typ,
          lhs_rel,
          dsize_rel,
          replacement_slice,
          slice_index,
          need_replacement,
          loc,
          scope,
          stmts,
          equiv_set,
          size_typ,
          dsize,
      ):
          # Do compile-time calculation of real index value if both the given
          # index value and the array length are known at compile time.
          known = False
          if isinstance(lhs_rel, int):
              # If the index and the array size are known then the real index
              # can be calculated at compile time.
              if lhs_rel == 0:
                  # Special-case 0 as nothing needing to be done.
                  known = True
              elif isinstance(dsize_rel, int):
                  known = True
                  # Calculate the real index.
                  wil = wrap_index_literal(lhs_rel, dsize_rel)
                  # If the given index value is between 0 and dsize then
                  # there's no need to rewrite anything.
                  if wil != lhs_rel:
                      if config.DEBUG_ARRAY_OPT >= 2:
                          print("Replacing slice to hard-code known slice size.")
                      # Indicate we will need to replace the slice var.
                      need_replacement = True
                      literal_var, literal_typ = self.gen_literal_slice_part(
                          wil, loc, scope, stmts, equiv_set
                      )
                      assert slice_index == 0 or slice_index == 1
                      if slice_index == 0:
                          replacement_slice.args = (
                              literal_var,
                              replacement_slice.args[1],
                          )
                      else:
                          replacement_slice.args = (
                              replacement_slice.args[0],
                              literal_var,
                          )
                      # Update lhs information with the negative removed.
                      lhs = replacement_slice.args[slice_index]
                      lhs_typ = literal_typ
                      lhs_rel = equiv_set.get_rel(lhs)
              elif lhs_rel < 0:
                  # Indicate we will need to replace the slice var.
                  need_replacement = True
                  if config.DEBUG_ARRAY_OPT >= 2:
                      print("Replacing slice due to known negative index.")
                  explicit_neg_var, explicit_neg_typ = self.gen_explicit_neg(
                      lhs,
                      lhs_rel,
                      lhs_typ,
                      size_typ,
                      loc,
                      scope,
                      dsize,
                      stmts,
                      equiv_set,
                  )
                  if slice_index == 0:
                      replacement_slice.args = (
                          explicit_neg_var,
                          replacement_slice.args[1],
                      )
                  else:
                      replacement_slice.args = (
                          replacement_slice.args[0],
                          explicit_neg_var,
                      )
                  # Update lhs information with the negative removed.
                  lhs = replacement_slice.args[slice_index]
                  lhs_typ = explicit_neg_typ
                  lhs_rel = equiv_set.get_rel(lhs)
          return (
              lhs,
              lhs_typ,
              lhs_rel,
              replacement_slice,
              need_replacement,
              known,
          )
  
      def slice_size(self, index, dsize, equiv_set, scope, stmts):
          """Reason about the size of a slice represented by the "index"
          variable, and return a variable that has this size data, or
          raise GuardException if it cannot reason about it.
  
          The computation takes care of negative values used in the slice
          with respect to the given dimensional size ("dsize").
  
          Extra statments required to produce the result are appended
          to parent function's stmts list.
          """
          loc = index.loc
          # Get the definition of the index variable.
          index_def = get_definition(self.func_ir, index)
          fname, mod_name = find_callname(
              self.func_ir, index_def, typemap=self.typemap
          )
          require(fname == 'slice' and mod_name in ('builtins'))
          require(len(index_def.args) == 2)
          lhs = index_def.args[0]
          rhs = index_def.args[1]
          size_typ = self.typemap[dsize.name]
          lhs_typ = self.typemap[lhs.name]
          rhs_typ = self.typemap[rhs.name]
  
          if config.DEBUG_ARRAY_OPT >= 2:
              print(f"slice_size index={index} dsize={dsize} "
                    f"index_def={index_def} lhs={lhs} rhs={rhs} "
                    f"size_typ={size_typ} lhs_typ={lhs_typ} rhs_typ={rhs_typ}")
  
          # Make a deepcopy of the original slice to use as the
          # replacement slice, which we will modify as necessary
          # below to convert all negative constants in the slice
          # to be relative to the dimension size.
          replacement_slice = copy.deepcopy(index_def)
          need_replacement = False
  
          # Fill in the left side of the slice's ":" with 0 if it wasn't
          # specified.
          if isinstance(lhs_typ, types.NoneType):
              zero_var = ir.Var(scope, mk_unique_var("zero"), loc)
              zero = ir.Const(0, loc)
              stmts.append(ir.Assign(value=zero, target=zero_var, loc=loc))
              self._define(equiv_set, zero_var, types.IntegerLiteral(0), zero)
              lhs = zero_var
              lhs_typ = types.IntegerLiteral(0)
              replacement_slice.args = (lhs, replacement_slice.args[1])
              need_replacement = True
              if config.DEBUG_ARRAY_OPT >= 2:
                  print("Replacing slice because lhs is None.")
  
          # Fill in the right side of the slice's ":" with the array
          # length if it wasn't specified.
          if isinstance(rhs_typ, types.NoneType):
              rhs = dsize
              rhs_typ = size_typ
              replacement_slice.args = (replacement_slice.args[0], rhs)
              need_replacement = True
              if config.DEBUG_ARRAY_OPT >= 2:
                  print("Replacing slice because lhs is None.")
  
          lhs_rel = equiv_set.get_rel(lhs)
          rhs_rel = equiv_set.get_rel(rhs)
          dsize_rel = equiv_set.get_rel(dsize)
          if config.DEBUG_ARRAY_OPT >= 2:
              print(
                  "lhs_rel", lhs_rel, "rhs_rel", rhs_rel, "dsize_rel", dsize_rel
              )
  
          # Update replacement slice with the real index value if we can
          # compute it at compile time.
          [
              lhs,
              lhs_typ,
              lhs_rel,
              replacement_slice,
              need_replacement,
              lhs_known,
          ] = self.update_replacement_slice(
              lhs,
              lhs_typ,
              lhs_rel,
              dsize_rel,
              replacement_slice,
              0,
              need_replacement,
              loc,
              scope,
              stmts,
              equiv_set,
              size_typ,
              dsize,
          )
          [
              rhs,
              rhs_typ,
              rhs_rel,
              replacement_slice,
              need_replacement,
              rhs_known,
          ] = self.update_replacement_slice(
              rhs,
              rhs_typ,
              rhs_rel,
              dsize_rel,
              replacement_slice,
              1,
              need_replacement,
              loc,
              scope,
              stmts,
              equiv_set,
              size_typ,
              dsize,
          )
          if config.DEBUG_ARRAY_OPT >= 2:
              print("lhs_known:", lhs_known)
              print("rhs_known:", rhs_known)
  
          # If neither of the parts of the slice were negative constants
          # then we don't need to do slice replacement in the IR.
          if not need_replacement:
              replacement_slice_var = None
          else:
              # Create a new var for the replacement slice.
              replacement_slice_var = ir.Var(
                  scope, mk_unique_var("replacement_slice"), loc
              )
              # Create a deepcopy of slice calltype so that when we change it
              # below the original isn't changed.  Make the types of the parts of
              # the slice intp.
              new_arg_typs = (types.intp, types.intp)
              rs_calltype = self.typemap[index_def.func.name].get_call_type(
                  self.context, new_arg_typs, {}
              )
              self.calltypes[replacement_slice] = rs_calltype
              stmts.append(
                  ir.Assign(
                      value=replacement_slice,
                      target=replacement_slice_var,
                      loc=loc,
                  )
              )
              # The type of the replacement slice is the same type as the
              # original.
              self.typemap[replacement_slice_var.name] = self.typemap[index.name]
  
          if config.DEBUG_ARRAY_OPT >= 2:
              print(
                  "after rewriting negatives",
                  "lhs_rel",
                  lhs_rel,
                  "rhs_rel",
                  rhs_rel,
              )
  
          if lhs_known and rhs_known:
              if config.DEBUG_ARRAY_OPT >= 2:
                  print("lhs and rhs known so return static size")
              return (
                  self.gen_static_slice_size(
                      lhs_rel, rhs_rel, loc, scope, stmts, equiv_set
                  ),
                  replacement_slice_var,
              )
  
          if (
              lhs_rel == 0
              and isinstance(rhs_rel, tuple)
              and equiv_set.is_equiv(dsize, rhs_rel[0])
              and rhs_rel[1] == 0
          ):
              return dsize, None
  
          slice_typ = types.intp
          orig_slice_typ = slice_typ
  
          size_var = ir.Var(scope, mk_unique_var("slice_size"), loc)
          size_val = ir.Expr.binop(operator.sub, rhs, lhs, loc=loc)
          self.calltypes[size_val] = signature(slice_typ, rhs_typ, lhs_typ)
          self._define(equiv_set, size_var, slice_typ, size_val)
          size_rel = equiv_set.get_rel(size_var)
          if config.DEBUG_ARRAY_OPT >= 2:
              print("size_rel", size_rel, type(size_rel))
  
          wrap_var = ir.Var(scope, mk_unique_var("wrap"), loc)
          wrap_def = ir.Global("wrap_index", wrap_index, loc=loc)
          fnty = get_global_func_typ(wrap_index)
          sig = self.context.resolve_function_type(
              fnty, (orig_slice_typ, size_typ), {}
          )
          self._define(equiv_set, wrap_var, fnty, wrap_def)
  
          def gen_wrap_if_not_known(val, val_typ, known):
              if not known:
                  var = ir.Var(scope, mk_unique_var("var"), loc)
                  var_typ = types.intp
                  new_value = ir.Expr.call(wrap_var, [val, dsize], {}, loc)
                  # def_res will be False if there is something unanalyzable
                  # that prevents a size association from being created.
                  self._define(equiv_set, var, var_typ, new_value)
                  self.calltypes[new_value] = sig
                  return (var, var_typ, new_value)
              else:
                  return (val, val_typ, None)
  
          var1, var1_typ, value1 = gen_wrap_if_not_known(lhs, lhs_typ, lhs_known)
          var2, var2_typ, value2 = gen_wrap_if_not_known(rhs, rhs_typ, rhs_known)
  
          stmts.append(ir.Assign(value=size_val, target=size_var, loc=loc))
          stmts.append(ir.Assign(value=wrap_def, target=wrap_var, loc=loc))
          if value1 is not None:
              stmts.append(ir.Assign(value=value1, target=var1, loc=loc))
          if value2 is not None:
              stmts.append(ir.Assign(value=value2, target=var2, loc=loc))
  
          post_wrap_size_var = ir.Var(
              scope, mk_unique_var("post_wrap_slice_size"), loc
          )
          post_wrap_size_val = ir.Expr.binop(operator.sub,
                                             var2,
                                             var1,
                                             loc=loc)
          self.calltypes[post_wrap_size_val] = signature(
              slice_typ, var2_typ, var1_typ
          )
          self._define(
              equiv_set, post_wrap_size_var, slice_typ, post_wrap_size_val
          )
  
          stmts.append(
              ir.Assign(
                  value=post_wrap_size_val, target=post_wrap_size_var, loc=loc
              )
          )
  
          # rel_map keeps a map of relative sizes that we have seen so
          # that if we compute the same relative sizes different times
          # in different ways we can associate those two instances
          # of the same relative size to the same equivalence class.
          if isinstance(size_rel, tuple):
              if config.DEBUG_ARRAY_OPT >= 2:
                  print("size_rel is tuple", equiv_set.rel_map)
              rel_map_entry = None
              for rme, rme_tuple in equiv_set.rel_map.items():
                  if rme[1] == size_rel[1] and equiv_set.is_equiv(
                      rme[0], size_rel[0]
                  ):
                      rel_map_entry = rme_tuple
                      break
  
              if rel_map_entry is not None:
                  # We have seen this relative size before so establish
                  # equivalence to the previous variable.
                  if config.DEBUG_ARRAY_OPT >= 2:
                      print("establishing equivalence to", rel_map_entry)
                  equiv_set.insert_equiv(size_var, rel_map_entry[0])
                  equiv_set.insert_equiv(post_wrap_size_var, rel_map_entry[1])
              else:
                  # The first time we've seen this relative size so
                  # remember the variable defining that size.
                  equiv_set.rel_map[size_rel] = (size_var, post_wrap_size_var)
  
          return post_wrap_size_var, replacement_slice_var
  
      def _index_to_shape(self, scope, equiv_set, var, ind_var):
          """For indexing like var[index] (either write or read), see if
          the index corresponds to a range/slice shape.
          Returns a 2-tuple where the first item is either None or a ir.Var
          to be used to replace the index variable in the outer getitem or
          setitem instruction.  The second item is also a tuple returning
          the shape and prepending instructions.
          """
          typ = self.typemap[var.name]
          require(isinstance(typ, types.ArrayCompatible))
          ind_typ = self.typemap[ind_var.name]
          ind_shape = equiv_set._get_shape(ind_var)
          var_shape = equiv_set._get_shape(var)
          if isinstance(ind_typ, types.SliceType):
              seq_typs = (ind_typ,)
          else:
              require(isinstance(ind_typ, types.BaseTuple))
              seq, op = find_build_sequence(self.func_ir, ind_var)
              require(op == "build_tuple")
              seq_typs = tuple(self.typemap[x.name] for x in seq)
          require(len(ind_shape) == len(seq_typs) == len(var_shape))
          stmts = []
  
          def to_shape(typ, index, dsize):
              if isinstance(typ, types.SliceType):
                  return self.slice_size(index, dsize, equiv_set, scope, stmts)
              elif isinstance(typ, types.Number):
                  return None, None
              else:
                  # unknown dimension size for this index,
                  # so we'll raise GuardException
                  require(False)
  
          shape_list = []
          index_var_list = []
          replace_index = False
          for (typ, size, dsize) in zip(seq_typs, ind_shape, var_shape):
              # Convert the given dimension of the get/setitem index expr.
              shape_part, index_var_part = to_shape(typ, size, dsize)
              shape_list.append(shape_part)
  
              # to_shape will return index_var_part as not None if a
              # replacement of the slice is required to convert from
              # negative indices to positive relative indices.
              if index_var_part is not None:
                  # Remember that we need to replace the build_tuple.
                  replace_index = True
                  index_var_list.append(index_var_part)
              else:
                  index_var_list.append(size)
  
          # If at least one of the dimensions required a new slice variable
          # then we'll need to replace the build_tuple for this get/setitem.
          if replace_index:
              # Multi-dimensional array access needs a replacement tuple built.
              if len(index_var_list) > 1:
                  # Make a variable to hold the new build_tuple.
                  replacement_build_tuple_var = ir.Var(
                      scope,
                      mk_unique_var("replacement_build_tuple"),
                      ind_shape[0].loc,
                  )
                  # Create the build tuple from the accumulated index vars above.
                  new_build_tuple = ir.Expr.build_tuple(
                      index_var_list, ind_shape[0].loc
                  )
                  stmts.append(
                      ir.Assign(
                          value=new_build_tuple,
                          target=replacement_build_tuple_var,
                          loc=ind_shape[0].loc,
                      )
                  )
                  # New build_tuple has same type as the original one.
                  self.typemap[replacement_build_tuple_var.name] = ind_typ
              else:
                  replacement_build_tuple_var = index_var_list[0]
          else:
              replacement_build_tuple_var = None
  
          shape = tuple(shape_list)
          require(not all(x is None for x in shape))
          shape = tuple(x for x in shape if x is not None)
          return (replacement_build_tuple_var,
                  ArrayAnalysis.AnalyzeResult(shape=shape, pre=stmts))
  
      def _analyze_op_getitem(self, scope, equiv_set, expr, lhs):
          result = self._index_to_shape(scope, equiv_set, expr.value, expr.index)
          if result[0] is not None:
              expr.index = result[0]
          return result[1]
  
      def _analyze_op_static_getitem(self, scope, equiv_set, expr, lhs):
          var = expr.value
          typ = self.typemap[var.name]
          if not isinstance(typ, types.BaseTuple):
              result = self._index_to_shape(
                  scope, equiv_set, expr.value, expr.index_var
              )
              if result[0] is not None:
                  expr.index_var = result[0]
              return result[1]
          shape = equiv_set._get_shape(var)
          if isinstance(expr.index, int):
              require(expr.index < len(shape))
              return ArrayAnalysis.AnalyzeResult(shape=shape[expr.index])
          elif isinstance(expr.index, slice):
              return ArrayAnalysis.AnalyzeResult(shape=shape[expr.index])
          require(False)
  
      def _analyze_op_unary(self, scope, equiv_set, expr, lhs):
          require(expr.fn in UNARY_MAP_OP)
          # for scalars, only + operator results in equivalence
          # for example, if "m = -n", m and n are not equivalent
          if self._isarray(expr.value.name) or expr.fn == operator.add:
              return ArrayAnalysis.AnalyzeResult(shape=expr.value)
          return None
  
      def _analyze_op_binop(self, scope, equiv_set, expr, lhs):
          require(expr.fn in BINARY_MAP_OP)
          return self._analyze_broadcast(
              scope, equiv_set, expr.loc, [expr.lhs, expr.rhs], expr.fn
          )
  
      def _analyze_op_inplace_binop(self, scope, equiv_set, expr, lhs):
          require(expr.fn in INPLACE_BINARY_MAP_OP)
          return self._analyze_broadcast(
              scope, equiv_set, expr.loc, [expr.lhs, expr.rhs], expr.fn
          )
  
      def _analyze_op_arrayexpr(self, scope, equiv_set, expr, lhs):
          return self._analyze_broadcast(
              scope, equiv_set, expr.loc, expr.list_vars(), None
          )
  
      def _analyze_op_build_tuple(self, scope, equiv_set, expr, lhs):
          # For the moment, we can't do anything with tuples that
          # contain multi-dimensional arrays, compared to array dimensions.
          # Return None to say we won't track this tuple if a part of it
          # is an array.
          for x in expr.items:
              if (
                  isinstance(x, ir.Var)
                  and isinstance(self.typemap[x.name], types.ArrayCompatible)
                  and self.typemap[x.name].ndim > 1
              ):
                  return None
  
          consts = []
          for var in expr.items:
              x = guard(find_const, self.func_ir, var)
              if x is not None:
                  consts.append(x)
              else:
                  break
          else:
              out = tuple([ir.Const(x, expr.loc) for x in consts])
              return ArrayAnalysis.AnalyzeResult(
                  shape=out,
                  rhs=ir.Const(tuple(consts), expr.loc)
              )
          # default return for non-const
          return ArrayAnalysis.AnalyzeResult(shape=tuple(expr.items))
  
      def _analyze_op_call(self, scope, equiv_set, expr, lhs):
          from numba.stencils.stencil import StencilFunc
  
          callee = expr.func
          callee_def = get_definition(self.func_ir, callee)
          if isinstance(
              callee_def, (ir.Global, ir.FreeVar)
          ) and is_namedtuple_class(callee_def.value):
              return ArrayAnalysis.AnalyzeResult(shape=tuple(expr.args))
          if isinstance(callee_def, (ir.Global, ir.FreeVar)) and isinstance(
              callee_def.value, StencilFunc
          ):
              args = expr.args
              return self._analyze_stencil(
                  scope,
                  equiv_set,
                  callee_def.value,
                  expr.loc,
                  args,
                  dict(expr.kws),
              )
  
          fname, mod_name = find_callname(
              self.func_ir, expr, typemap=self.typemap
          )
          added_mod_name = False
          # call via attribute (i.e. array.func)
          if isinstance(mod_name, ir.Var) and isinstance(
              self.typemap[mod_name.name], types.ArrayCompatible
          ):
              args = [mod_name] + expr.args
              mod_name = "numpy"
              # Remember that args and expr.args don't alias.
              added_mod_name = True
          else:
              args = expr.args
          fname = "_analyze_op_call_{}_{}".format(mod_name, fname).replace(
              ".", "_"
          )
          if fname in UFUNC_MAP_OP:  # known numpy ufuncs
              return self._analyze_broadcast(scope, equiv_set,
                                             expr.loc, args, None)
          else:
              try:
                  fn = getattr(self, fname)
              except AttributeError:
                  return None
              result = guard(
                  fn,
                  scope=scope,
                  equiv_set=equiv_set,
                  loc=expr.loc,
                  args=args,
                  kws=dict(expr.kws),
              )
              # We want the ability for function fn to modify arguments.
              # If args and expr.args don't alias then we need the extra
              # step of assigning back into expr.args from the args that
              # was passed to fn.
              if added_mod_name:
                  expr.args = args[1:]
              return result
  
      def _analyze_op_call_builtins_len(self, scope, equiv_set, loc, args, kws):
          # python 3 version of len()
          require(len(args) == 1)
          var = args[0]
          typ = self.typemap[var.name]
          require(isinstance(typ, types.ArrayCompatible))
          shape = equiv_set._get_shape(var)
          return ArrayAnalysis.AnalyzeResult(shape=shape[0], rhs=shape[0])
  
      def _analyze_op_call_numba_parfors_array_analysis_assert_equiv(
          self, scope, equiv_set, loc, args, kws
      ):
          equiv_set.insert_equiv(*args[1:])
          return None
  
      def _analyze_op_call_numba_parfors_array_analysis_wrap_index(
          self, scope, equiv_set, loc, args, kws
      ):
          """ Analyze wrap_index calls added by a previous run of
              Array Analysis
          """
          require(len(args) == 2)
          # Two parts to wrap index, the specified slice size...
          slice_size = args[0].name
          # ...and the size of the dimension.
          dim_size = args[1].name
          # Get the equivalence class ids for both.
          slice_eq = equiv_set._get_or_add_ind(slice_size)
          dim_eq = equiv_set._get_or_add_ind(dim_size)
          # See if a previous wrap_index calls we've analyzed maps from
          # the same pair of equivalence class ids for slice and dim size.
          if (slice_eq, dim_eq) in equiv_set.wrap_map:
              wrap_ind = equiv_set.wrap_map[(slice_eq, dim_eq)]
              require(wrap_ind in equiv_set.ind_to_var)
              vs = equiv_set.ind_to_var[wrap_ind]
              require(vs != [])
              # Return the shape of the variable from the previous wrap_index.
              return ArrayAnalysis.AnalyzeResult(shape=(vs[0],))
          else:
              # We haven't seen this combination of slice and dim
              # equivalence class ids so return a WrapIndexMeta so that
              # _analyze_inst can establish the connection to the lhs var.
              return ArrayAnalysis.AnalyzeResult(
                  shape=WrapIndexMeta(slice_eq, dim_eq)
              )
  
      def _analyze_numpy_create_array(self, scope, equiv_set, loc, args, kws):
          shape_var = None
          if len(args) > 0:
              shape_var = args[0]
          elif "shape" in kws:
              shape_var = kws["shape"]
          if shape_var:
              return ArrayAnalysis.AnalyzeResult(shape=shape_var)
          raise errors.UnsupportedRewriteError(
              "Must specify a shape for array creation",
              loc=loc,
          )
  
      def _analyze_op_call_numpy_empty(self, scope, equiv_set, loc, args, kws):
          return self._analyze_numpy_create_array(
              scope, equiv_set, loc, args, kws
          )
  
      def _analyze_op_call_numba_np_unsafe_ndarray_empty_inferred(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_numpy_create_array(
              scope, equiv_set, loc, args, kws
          )
  
      def _analyze_op_call_numpy_zeros(self, scope, equiv_set, loc, args, kws):
          return self._analyze_numpy_create_array(
              scope, equiv_set, loc, args, kws
          )
  
      def _analyze_op_call_numpy_ones(self, scope, equiv_set, loc, args, kws):
          return self._analyze_numpy_create_array(
              scope, equiv_set, loc, args, kws
          )
  
      def _analyze_op_call_numpy_eye(self, scope, equiv_set, loc, args, kws):
          if len(args) > 0:
              N = args[0]
          elif "N" in kws:
              N = kws["N"]
          else:
              raise errors.UnsupportedRewriteError(
                  "Expect one argument (or 'N') to eye function",
                  loc=loc,
              )
          if "M" in kws:
              M = kws["M"]
          else:
              M = N
          return ArrayAnalysis.AnalyzeResult(shape=(N, M))
  
      def _analyze_op_call_numpy_identity(
          self, scope, equiv_set, loc, args, kws
      ):
          assert len(args) > 0
          N = args[0]
          return ArrayAnalysis.AnalyzeResult(shape=(N, N))
  
      def _analyze_op_call_numpy_diag(self, scope, equiv_set, loc, args, kws):
          # We can only reason about the output shape when the input is 1D or
          # square 2D.
          assert len(args) > 0
          a = args[0]
          assert isinstance(a, ir.Var)
          atyp = self.typemap[a.name]
          if isinstance(atyp, types.ArrayCompatible):
              if atyp.ndim == 2:
                  if "k" in kws:  # will proceed only when k = 0 or absent
                      k = kws["k"]
                      if not equiv_set.is_equiv(k, 0):
                          return None
                  (m, n) = equiv_set._get_shape(a)
                  if equiv_set.is_equiv(m, n):
                      return ArrayAnalysis.AnalyzeResult(shape=(m,))
              elif atyp.ndim == 1:
                  (m,) = equiv_set._get_shape(a)
                  return ArrayAnalysis.AnalyzeResult(shape=(m, m))
          return None
  
      def _analyze_numpy_array_like(self, scope, equiv_set, args, kws):
          assert len(args) > 0
          var = args[0]
          typ = self.typemap[var.name]
          if isinstance(typ, types.Integer):
              return ArrayAnalysis.AnalyzeResult(shape=(1,))
          elif isinstance(typ, types.ArrayCompatible) and equiv_set.has_shape(
              var
          ):
              return ArrayAnalysis.AnalyzeResult(shape=var)
          return None
  
      def _analyze_op_call_numpy_ravel(self, scope, equiv_set, loc, args, kws):
          assert len(args) == 1
          var = args[0]
          typ = self.typemap[var.name]
          assert isinstance(typ, types.ArrayCompatible)
          # output array is same shape as input if input is 1D
          if typ.ndim == 1 and equiv_set.has_shape(var):
              if typ.layout == "C":
                  # output is the same as input (no copy) for 'C' layout
                  # optimize out the call
                  return ArrayAnalysis.AnalyzeResult(shape=var, rhs=var)
              else:
                  return ArrayAnalysis.AnalyzeResult(shape=var)
          # TODO: handle multi-D input arrays (calc array size)
          return None
  
      def _analyze_op_call_numpy_copy(self, scope, equiv_set, loc, args, kws):
          return self._analyze_numpy_array_like(scope, equiv_set, args, kws)
  
      def _analyze_op_call_numpy_empty_like(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_numpy_array_like(scope, equiv_set, args, kws)
  
      def _analyze_op_call_numpy_zeros_like(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_numpy_array_like(scope, equiv_set, args, kws)
  
      def _analyze_op_call_numpy_ones_like(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_numpy_array_like(scope, equiv_set, args, kws)
  
      def _analyze_op_call_numpy_full_like(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_numpy_array_like(scope, equiv_set, args, kws)
  
      def _analyze_op_call_numpy_asfortranarray(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_numpy_array_like(scope, equiv_set, args, kws)
  
      def _analyze_op_call_numpy_reshape(self, scope, equiv_set, loc, args, kws):
          n = len(args)
          assert n > 1
          if n == 2:
              typ = self.typemap[args[1].name]
              if isinstance(typ, types.BaseTuple):
                  return ArrayAnalysis.AnalyzeResult(shape=args[1])
  
          # Reshape is allowed to take one argument that has the value <0.
          # This means that the size of that dimension should be inferred from
          # the size of the array being reshaped and the other dimensions
          # specified.  Our general approach here is to see if the reshape
          # has any <0 arguments.  If it has more than one then throw a
          # ValueError.  If exactly one <0 argument is found, remember its
          # argument index.
          stmts = []
          neg_one_index = -1
          for arg_index in range(1, len(args)):
              reshape_arg = args[arg_index]
              reshape_arg_def = guard(get_definition, self.func_ir, reshape_arg)
              if isinstance(reshape_arg_def, ir.Const):
                  if reshape_arg_def.value < 0:
                      if neg_one_index == -1:
                          neg_one_index = arg_index
                      else:
                          msg = ("The reshape API may only include one negative"
                                 " argument.")
                          raise errors.UnsupportedRewriteError(
                              msg, loc=reshape_arg.loc
                          )
  
          if neg_one_index >= 0:
              # If exactly one <0 argument to reshape was found, then we are
              # going to insert code to calculate the missing dimension and then
              # replace the negative with the calculated size.  We do this
              # because we can't let array equivalence analysis think that some
              # array has a negative dimension size.
              loc = args[0].loc
              # Create a variable to hold the size of the array being reshaped.
              calc_size_var = ir.Var(scope, mk_unique_var("calc_size_var"), loc)
              self.typemap[calc_size_var.name] = types.intp
              # Assign the size of the array calc_size_var.
              init_calc_var = ir.Assign(
                  ir.Expr.getattr(args[0], "size", loc), calc_size_var, loc
              )
              stmts.append(init_calc_var)
              # For each other dimension, divide the current size by the
              # specified dimension size.  Once all such dimensions have been
              # done then what is left is the size of the negative dimension.
              for arg_index in range(1, len(args)):
                  # Skip the negative dimension.
                  if arg_index == neg_one_index:
                      continue
                  div_calc_size_var = ir.Var(
                      scope, mk_unique_var("calc_size_var"), loc
                  )
                  self.typemap[div_calc_size_var.name] = types.intp
                  # Calculate the next size as current size // the current arg's
                  # dimension size.
                  new_binop = ir.Expr.binop(
                      operator.floordiv, calc_size_var, args[arg_index], loc
                  )
                  div_calc = ir.Assign(new_binop, div_calc_size_var, loc)
                  self.calltypes[new_binop] = signature(
                      types.intp, types.intp, types.intp
                  )
                  stmts.append(div_calc)
                  calc_size_var = div_calc_size_var
              # Put the calculated value back into the reshape arguments,
              # replacing the negative.
              args[neg_one_index] = calc_size_var
  
          return ArrayAnalysis.AnalyzeResult(shape=tuple(args[1:]), pre=stmts)
  
      def _analyze_op_call_numpy_transpose(
          self, scope, equiv_set, loc, args, kws
      ):
          in_arr = args[0]
          typ = self.typemap[in_arr.name]
          assert isinstance(
              typ, types.ArrayCompatible
          ), "Invalid np.transpose argument"
          shape = equiv_set._get_shape(in_arr)
          if len(args) == 1:
              return ArrayAnalysis.AnalyzeResult(shape=tuple(reversed(shape)))
          axes = [guard(find_const, self.func_ir, a) for a in args[1:]]
          if isinstance(axes[0], tuple):
              axes = list(axes[0])
          if None in axes:
              return None
          ret = [shape[i] for i in axes]
          return ArrayAnalysis.AnalyzeResult(shape=tuple(ret))
  
      def _analyze_op_call_numpy_random_rand(
          self, scope, equiv_set, loc, args, kws
      ):
          if len(args) > 0:
              return ArrayAnalysis.AnalyzeResult(shape=tuple(args))
          return None
  
      def _analyze_op_call_numpy_random_randn(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_call_numpy_random_rand(
              scope, equiv_set, loc, args, kws
          )
  
      def _analyze_op_numpy_random_with_size(
          self, pos, scope, equiv_set, args, kws
      ):
          if "size" in kws:
              return ArrayAnalysis.AnalyzeResult(shape=kws["size"])
          if len(args) > pos:
              return ArrayAnalysis.AnalyzeResult(shape=args[pos])
          return None
  
      def _analyze_op_call_numpy_random_ranf(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              0, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_random_sample(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              0, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_sample(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              0, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_random(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              0, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_standard_normal(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              0, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_chisquare(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              1, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_weibull(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              1, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_power(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              1, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_geometric(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              1, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_exponential(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              1, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_poisson(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              1, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_rayleigh(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              1, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_normal(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_uniform(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_beta(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_binomial(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_f(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_gamma(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_lognormal(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_laplace(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_randint(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              2, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_random_triangular(
          self, scope, equiv_set, loc, args, kws
      ):
          return self._analyze_op_numpy_random_with_size(
              3, scope, equiv_set, args, kws
          )
  
      def _analyze_op_call_numpy_concatenate(
          self, scope, equiv_set, loc, args, kws
      ):
          assert len(args) > 0
          loc = args[0].loc
          seq, op = find_build_sequence(self.func_ir, args[0])
          n = len(seq)
          require(n > 0)
          axis = 0
          if "axis" in kws:
              if isinstance(kws["axis"], int):  # internal use only
                  axis = kws["axis"]
              else:
                  axis = find_const(self.func_ir, kws["axis"])
          elif len(args) > 1:
              axis = find_const(self.func_ir, args[1])
          require(isinstance(axis, int))
          require(op == "build_tuple")
          shapes = [equiv_set._get_shape(x) for x in seq]
          if axis < 0:
              axis = len(shapes[0]) + axis
          require(0 <= axis < len(shapes[0]))
          asserts = []
          new_shape = []
          if n == 1:  # from one array N-dimension to (N-1)-dimension
              shape = shapes[0]
              # first size is the count, pop it out of shapes
              n = equiv_set.get_equiv_const(shapes[0])
              shape.pop(0)
              for i in range(len(shape)):
                  if i == axis:
                      m = equiv_set.get_equiv_const(shape[i])
                      size = m * n if (m and n) else None
                  else:
                      size = self._sum_size(equiv_set, shapes[0])
              new_shape.append(size)
          else:  # from n arrays N-dimension to N-dimension
              for i in range(len(shapes[0])):
                  if i == axis:
                      size = self._sum_size(
                          equiv_set, [shape[i] for shape in shapes]
                      )
                  else:
                      sizes = [shape[i] for shape in shapes]
                      asserts.append(
                          self._call_assert_equiv(scope, loc, equiv_set, sizes)
                      )
                      size = sizes[0]
                  new_shape.append(size)
          return ArrayAnalysis.AnalyzeResult(
              shape=tuple(new_shape),
              pre=sum(asserts, [])
          )
  
      def _analyze_op_call_numpy_stack(self, scope, equiv_set, loc, args, kws):
          assert len(args) > 0
          loc = args[0].loc
          seq, op = find_build_sequence(self.func_ir, args[0])
          n = len(seq)
          require(n > 0)
          axis = 0
          if "axis" in kws:
              if isinstance(kws["axis"], int):  # internal use only
                  axis = kws["axis"]
              else:
                  axis = find_const(self.func_ir, kws["axis"])
          elif len(args) > 1:
              axis = find_const(self.func_ir, args[1])
          require(isinstance(axis, int))
          # only build_tuple can give reliable count
          require(op == "build_tuple")
          shapes = [equiv_set._get_shape(x) for x in seq]
          asserts = self._call_assert_equiv(scope, loc, equiv_set, seq)
          shape = shapes[0]
          if axis < 0:
              axis = len(shape) + axis + 1
          require(0 <= axis <= len(shape))
          new_shape = list(shape[0:axis]) + [n] + list(shape[axis:])
          return ArrayAnalysis.AnalyzeResult(shape=tuple(new_shape), pre=asserts)
  
      def _analyze_op_call_numpy_vstack(self, scope, equiv_set, loc, args, kws):
          assert len(args) == 1
          seq, op = find_build_sequence(self.func_ir, args[0])
          n = len(seq)
          require(n > 0)
          typ = self.typemap[seq[0].name]
          require(isinstance(typ, types.ArrayCompatible))
          if typ.ndim < 2:
              return self._analyze_op_call_numpy_stack(
                  scope, equiv_set, loc, args, kws
              )
          else:
              kws["axis"] = 0
              return self._analyze_op_call_numpy_concatenate(
                  scope, equiv_set, loc, args, kws
              )
  
      def _analyze_op_call_numpy_hstack(self, scope, equiv_set, loc, args, kws):
          assert len(args) == 1
          seq, op = find_build_sequence(self.func_ir, args[0])
          n = len(seq)
          require(n > 0)
          typ = self.typemap[seq[0].name]
          require(isinstance(typ, types.ArrayCompatible))
          if typ.ndim < 2:
              kws["axis"] = 0
          else:
              kws["axis"] = 1
          return self._analyze_op_call_numpy_concatenate(
              scope, equiv_set, loc, args, kws
          )
  
      def _analyze_op_call_numpy_dstack(self, scope, equiv_set, loc, args, kws):
          assert len(args) == 1
          seq, op = find_build_sequence(self.func_ir, args[0])
          n = len(seq)
          require(n > 0)
          typ = self.typemap[seq[0].name]
          require(isinstance(typ, types.ArrayCompatible))
          if typ.ndim == 1:
              kws["axis"] = 1
              result = self._analyze_op_call_numpy_stack(
                  scope, equiv_set, loc, args, kws
              )
              require(result)
              result.kwargs['shape'] = tuple([1] + list(result.kwargs['shape']))
              return result
          elif typ.ndim == 2:
              kws["axis"] = 2
              return self._analyze_op_call_numpy_stack(
                  scope, equiv_set, loc, args, kws
              )
          else:
              kws["axis"] = 2
              return self._analyze_op_call_numpy_concatenate(
                  scope, equiv_set, loc, args, kws
              )
  
      def _analyze_op_call_numpy_cumsum(self, scope, equiv_set, loc, args, kws):
          # TODO
          return None
  
      def _analyze_op_call_numpy_cumprod(self, scope, equiv_set, loc, args, kws):
          # TODO
          return None
  
      def _analyze_op_call_numpy_linspace(
          self, scope, equiv_set, loc, args, kws
      ):
          n = len(args)
          num = 50
          if n > 2:
              num = args[2]
          elif "num" in kws:
              num = kws["num"]
          return ArrayAnalysis.AnalyzeResult(shape=(num,))
  
      def _analyze_op_call_numpy_dot(self, scope, equiv_set, loc, args, kws):
          n = len(args)
          assert n >= 2
          loc = args[0].loc
          require(all([self._isarray(x.name) for x in args]))
          typs = [self.typemap[x.name] for x in args]
          dims = [ty.ndim for ty in typs]
          require(all(x > 0 for x in dims))
          if dims[0] == 1 and dims[1] == 1:
              return None
          shapes = [equiv_set._get_shape(x) for x in args]
          if dims[0] == 1:
              asserts = self._call_assert_equiv(
                  scope, loc, equiv_set, [shapes[0][0], shapes[1][-2]]
              )
              return ArrayAnalysis.AnalyzeResult(
                  shape=tuple(shapes[1][0:-2] + shapes[1][-1:]),
                  pre=asserts
              )
          if dims[1] == 1:
              asserts = self._call_assert_equiv(
                  scope, loc, equiv_set, [shapes[0][-1], shapes[1][0]]
              )
              return ArrayAnalysis.AnalyzeResult(
                  shape=tuple(shapes[0][0:-1]),
                  pre=asserts
              )
          if dims[0] == 2 and dims[1] == 2:
              asserts = self._call_assert_equiv(
                  scope, loc, equiv_set, [shapes[0][1], shapes[1][0]]
              )
              return ArrayAnalysis.AnalyzeResult(
                  shape=(shapes[0][0], shapes[1][1]),
                  pre=asserts
              )
          if dims[0] > 2:  # TODO: handle higher dimension cases
              pass
          return None
  
      def _analyze_stencil(self, scope, equiv_set, stencil_func, loc, args, kws):
          # stencil requires that all relatively indexed array arguments are
          # of same size
          std_idx_arrs = stencil_func.options.get("standard_indexing", ())
          kernel_arg_names = stencil_func.kernel_ir.arg_names
          if isinstance(std_idx_arrs, str):
              std_idx_arrs = (std_idx_arrs,)
          rel_idx_arrs = []
          assert len(args) > 0 and len(args) == len(kernel_arg_names)
          for arg, var in zip(kernel_arg_names, args):
              typ = self.typemap[var.name]
              if isinstance(typ, types.ArrayCompatible) and not (
                  arg in std_idx_arrs
              ):
                  rel_idx_arrs.append(var)
          n = len(rel_idx_arrs)
          require(n > 0)
          asserts = self._call_assert_equiv(scope, loc, equiv_set, rel_idx_arrs)
          shape = equiv_set.get_shape(rel_idx_arrs[0])
          return ArrayAnalysis.AnalyzeResult(shape=shape, pre=asserts)
  
      def _analyze_op_call_numpy_linalg_inv(
          self, scope, equiv_set, loc, args, kws
      ):
          require(len(args) >= 1)
          return ArrayAnalysis.AnalyzeResult(shape=equiv_set._get_shape(args[0]))
  
      def _insert_runtime_broadcast_call(self, scope, loc, arrs, max_dim):
          pre = []
  
          typs = [self.typemap[x.name] for x in arrs]
          dims = [self.typemap[x.name].ndim for x in arrs]
          max_dim = max(dims)
  
          runtime_broadcast_shape = ir.Var(
              scope, mk_unique_var("runtime_broadcast_shape"), loc
          )
          runtime_broadcast_type = types.containers.List(types.intp)
          self.typemap[runtime_broadcast_shape.name] = runtime_broadcast_type
  
          func_var = ir.Var(scope, mk_unique_var("runtime_broadcast_call"), loc)
          func_def = ir.Global(
              "runtime_broadcast_assert_shapes",
              runtime_broadcast_assert_shapes,
              loc=loc
          )
          pre.append(
              ir.Assign(value=func_def, target=func_var, loc=loc)
          )
          func_fnty = get_global_func_typ(runtime_broadcast_assert_shapes)
          print("func_fnty:", func_fnty)
          self.typemap[func_var.name] = func_fnty
          fargs = [types.literal(max_dim)] + typs
          print("typs:", typs)
          print("dims:", dims, max_dim)
          print("fargs:", fargs)
          func_sig = self.context.resolve_function_type(func_fnty, fargs, {})
          print("func_sig:", func_sig)
  
          max_dim_var = ir.Var(scope, mk_unique_var("max_dim"), loc)
          max_dim_val = ir.Const(max_dim, loc)
          max_dim_typ = types.IntegerLiteral(max_dim)
          self.typemap[max_dim_var.name] = max_dim_typ
          pre.append(
              ir.Assign(value=max_dim_val, target=max_dim_var, loc=loc)
          )
  
          func_call = ir.Expr.call(func_var, [max_dim_var] + arrs, {}, loc)
  
          self.calltypes[func_call] = func_sig
          pre.append(
              ir.Assign(value=func_call, target=runtime_broadcast_shape, loc=loc)
          )
  
          return ArrayAnalysis.AnalyzeResult(
              shape=runtime_broadcast_shape,
              pre=pre
          )
  
      def _analyze_broadcast(self, scope, equiv_set, loc, args, fn):
          """Infer shape equivalence of arguments based on Numpy broadcast rules
          and return shape of output
          https://docs.scipy.org/doc/numpy/user/basics.broadcasting.html
          """
          tups = list(filter(lambda a: self._istuple(a.name), args))
          # Here we have a tuple concatenation.
          if len(tups) == 2 and fn.__name__ == 'add':
              # If either of the tuples is empty then the resulting shape
              # is just the other tuple.
              tup0typ = self.typemap[tups[0].name]
              tup1typ = self.typemap[tups[1].name]
              if tup0typ.count == 0:
                  return ArrayAnalysis.AnalyzeResult(
                      shape=equiv_set.get_shape(tups[1])
                  )
              if tup1typ.count == 0:
                  return ArrayAnalysis.AnalyzeResult(
                      shape=equiv_set.get_shape(tups[0])
                  )
  
              try:
                  shapes = [equiv_set.get_shape(x) for x in tups]
                  if None in shapes:
                      return None
                  concat_shapes = sum(shapes, ())
                  return ArrayAnalysis.AnalyzeResult(
                      shape=concat_shapes
                  )
              except GuardException:
                  return None
  
          # else arrays
          arrs = list(filter(lambda a: self._isarray(a.name), args))
          require(len(arrs) > 0)
          names = [x.name for x in arrs]
          dims = [self.typemap[x.name].ndim for x in arrs]
          max_dim = max(dims)
          require(max_dim > 0)
          try:
              shapes = [equiv_set.get_shape(x) for x in arrs]
          except GuardException:
              return ArrayAnalysis.AnalyzeResult(
                  shape=arrs[0],
                  pre=self._call_assert_equiv(scope, loc, equiv_set, arrs)
              )
          if None not in shapes:
              return self._broadcast_assert_shapes(
                  scope, equiv_set, loc, shapes, names
              )
          else:
              return self._insert_runtime_broadcast_call(
                  scope, loc, arrs, max_dim
              )
  
      def _broadcast_assert_shapes(self, scope, equiv_set, loc, shapes, names):
          """Produce assert_equiv for sizes in each dimension, taking into
          account of dimension coercion and constant size of 1.
          """
          asserts = []
          new_shape = []
          max_dim = max([len(shape) for shape in shapes])
          const_size_one = None
          for i in range(max_dim):
              sizes = []
              size_names = []
              for name, shape in zip(names, shapes):
                  if i < len(shape):
                      size = shape[len(shape) - 1 - i]
                      const_size = equiv_set.get_equiv_const(size)
                      if const_size == 1:
                          const_size_one = size
                      else:
                          sizes.append(size)  # non-1 size to front
                          size_names.append(name)
              if sizes == []:
                  assert const_size_one is not None
                  sizes.append(const_size_one)
                  size_names.append("1")
              asserts.append(
                  self._call_assert_equiv(
                      scope, loc, equiv_set, sizes, names=size_names
                  )
              )
              new_shape.append(sizes[0])
          return ArrayAnalysis.AnalyzeResult(
              shape=tuple(reversed(new_shape)),
              pre=sum(asserts, [])
          )
  
      def _call_assert_equiv(self, scope, loc, equiv_set, args, names=None):
          insts = self._make_assert_equiv(
              scope, loc, equiv_set, args, names=names
          )
          if len(args) > 1:
              equiv_set.insert_equiv(*args)
          return insts
  
      def _make_assert_equiv(self, scope, loc, equiv_set, _args, names=None):
          # filter out those that are already equivalent
          if config.DEBUG_ARRAY_OPT >= 2:
              print("make_assert_equiv:", _args, names)
          if names is None:
              names = [x.name for x in _args]
          args = []
          arg_names = []
          for name, x in zip(names, _args):
              if config.DEBUG_ARRAY_OPT >= 2:
                  print("name, x:", name, x)
              seen = False
              for y in args:
                  if config.DEBUG_ARRAY_OPT >= 2:
                      print("is equiv to?", y, equiv_set.is_equiv(x, y))
                  if equiv_set.is_equiv(x, y):
                      seen = True
                      break
              if not seen:
                  args.append(x)
                  arg_names.append(name)
  
          # no assertion necessary if there are less than two
          if len(args) < 2:
              if config.DEBUG_ARRAY_OPT >= 2:
                  print(
                      "Will not insert assert_equiv as args are known to be "
                      "equivalent."
                  )
              return []
  
          msg = "Sizes of {} do not match on {}".format(
              ", ".join(arg_names), loc
          )
          msg_val = ir.Const(msg, loc)
          msg_typ = types.StringLiteral(msg)
          msg_var = ir.Var(scope, mk_unique_var("msg"), loc)
          self.typemap[msg_var.name] = msg_typ
          argtyps = tuple([msg_typ] + [self.typemap[x.name] for x in args])
  
          # assert_equiv takes vararg, which requires a tuple as argument type
          tup_typ = types.StarArgTuple.from_types(argtyps)
  
          # prepare function variable whose type may vary since it takes vararg
          assert_var = ir.Var(scope, mk_unique_var("assert"), loc)
          assert_def = ir.Global("assert_equiv", assert_equiv, loc=loc)
          fnty = get_global_func_typ(assert_equiv)
          sig = self.context.resolve_function_type(fnty, (tup_typ,), {})
          self._define(equiv_set, assert_var, fnty, assert_def)
  
          # The return value from assert_equiv is always of none type.
          var = ir.Var(scope, mk_unique_var("ret"), loc)
          value = ir.Expr.call(assert_var, [msg_var] + args, {}, loc=loc)
          self._define(equiv_set, var, types.none, value)
          self.calltypes[value] = sig
  
          return [
              ir.Assign(value=msg_val, target=msg_var, loc=loc),
              ir.Assign(value=assert_def, target=assert_var, loc=loc),
              ir.Assign(value=value, target=var, loc=loc),
          ]
  
      def _gen_shape_call(self, equiv_set, var, ndims, shape, post):
          # attr call: A_sh_attr = getattr(A, shape)
          if isinstance(shape, ir.Var):
              shape = equiv_set.get_shape(shape)
  
          # already a tuple variable that contains size
          if isinstance(shape, ir.Var):
              attr_var = shape
              shape_attr_call = None
              shape = None
          elif isinstance(shape, ir.Arg):
              attr_var = var
              shape_attr_call = None
              shape = None
          else:
              shape_attr_call = ir.Expr.getattr(var, "shape", var.loc)
              attr_var = ir.Var(
                  var.scope, mk_unique_var("{}_shape".format(var.name)), var.loc
              )
              shape_attr_typ = types.containers.UniTuple(types.intp, ndims)
          size_vars = []
          use_attr_var = False
          # trim shape tuple if it is more than ndim
          if shape:
              nshapes = len(shape)
              if ndims < nshapes:
                  shape = shape[(nshapes - ndims) :]
          for i in range(ndims):
              skip = False
              if shape and shape[i]:
                  if isinstance(shape[i], ir.Var):
                      typ = self.typemap[shape[i].name]
                      if isinstance(typ, (types.Number, types.SliceType)):
                          size_var = shape[i]
                          skip = True
                  else:
                      if isinstance(shape[i], int):
                          size_val = ir.Const(shape[i], var.loc)
                      else:
                          size_val = shape[i]
                      assert isinstance(size_val, ir.Const)
                      size_var = ir.Var(
                          var.scope,
                          mk_unique_var("{}_size{}".format(var.name, i)),
                          var.loc,
                      )
                      post.append(ir.Assign(size_val, size_var, var.loc))
                      self._define(equiv_set, size_var, types.intp, size_val)
                      skip = True
              if not skip:
                  # get size: Asize0 = A_sh_attr[0]
                  size_var = ir.Var(
                      var.scope,
                      mk_unique_var("{}_size{}".format(var.name, i)),
                      var.loc,
                  )
                  getitem = ir.Expr.static_getitem(attr_var, i, None, var.loc)
                  use_attr_var = True
                  self.calltypes[getitem] = None
                  post.append(ir.Assign(getitem, size_var, var.loc))
                  self._define(equiv_set, size_var, types.intp, getitem)
              size_vars.append(size_var)
          if use_attr_var and shape_attr_call:
              # only insert shape call if there is any getitem call
              post.insert(0, ir.Assign(shape_attr_call, attr_var, var.loc))
              self._define(equiv_set, attr_var, shape_attr_typ, shape_attr_call)
          return tuple(size_vars)
  
      def _isarray(self, varname):
          typ = self.typemap[varname]
          return isinstance(typ, types.npytypes.Array) and typ.ndim > 0
  
      def _istuple(self, varname):
          typ = self.typemap[varname]
          return isinstance(typ, types.BaseTuple)
  
      def _sum_size(self, equiv_set, sizes):
          """Return the sum of the given list of sizes if they are all equivalent
          to some constant, or None otherwise.
          """
          s = 0
          for size in sizes:
              n = equiv_set.get_equiv_const(size)
              if n is None:
                  return None
              else:
                  s += n
          return s
  
  
  UNARY_MAP_OP = list(npydecl.NumpyRulesUnaryArrayOperator._op_map.keys()) + [
      operator.pos
  ]
  BINARY_MAP_OP = npydecl.NumpyRulesArrayOperator._op_map.keys()
  INPLACE_BINARY_MAP_OP = npydecl.NumpyRulesInplaceArrayOperator._op_map.keys()
  UFUNC_MAP_OP = [f.__name__ for f in npydecl.supported_ufuncs]
  