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 ()
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.
        
numba/parfors/array_analysis.py