Repository: numba/numba

Issue:
parfors issue with recent patch
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

This:
```python
from numba import njit
import numpy as np

N = 5
M = 17

@njit(parallel=True)
def test_func(nthreads):
    buf = np.zeros((M, N))
    return buf

test_func(23)
```
now reports 
```
numba/numba/lowering.py", line 1154, in getvar
    return self.varmap[name]
numba.errors.LoweringError: Failed in nopython mode pipeline (step: nopython mode backend)
'$12call_method.5_size0.1'

File "foo123321.py", line 9:
def test_func(nthreads):
    buf = np.zeros((M, N))
```
on master as of 71ebcc9.

bisect points to `[b7300f5f0e4c8b09cf58716101392c916b54d669] Experiment with union type` but it might be that in combination with other recent things.


Discussion:
Specifically PR https://github.com/numba/numba/pull/4910. Doing a `git revert -m 1 d46a516e1852a3b66226f63cac9a96a1521fba8a` (and fixing the trivial conflicts) fixes this. Perhaps if a simple fix cannot be found that PR should be reverted? 
#4910 adds an important feature, so reverting isn't really feasible. A fix will be found.
I've found the bit of code that's triggering it, it's here: https://github.com/numba/numba/blob/0072af34ff11b1337870458d0bcc3fa64d78c5c6/numba/array_analysis.py#L1501-L1511

commenting that out makes the sample above pass. I think I've just done this analysis section wrong in the midst of writing the loop body versioning code. @DrTodd13 any chance you could perhaps take a look as well please, think you are more familiar with this code path than me? Thanks!

Resolving patch:
diff --git a/numba/array_analysis.py b/numba/array_analysis.py
--- a/numba/array_analysis.py
+++ b/numba/array_analysis.py
@@ -328,7 +328,7 @@ class ShapeEquivSet(EquivSet):
     """
 
     def __init__(self, typemap, defs=None, ind_to_var=None,
-                 obj_to_ind=None, ind_to_obj=None, next_id=0):
+                 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.
@@ -342,6 +342,8 @@ def __init__(self, typemap, defs=None, ind_to_var=None,
         # 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)
 
@@ -359,11 +361,12 @@ def clone(self):
             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)
+            next_id=self.next_ind,
+            ind_to_const=copy.deepcopy(self.ind_toconst))
 
     def __repr__(self):
-        return "ShapeEquivSet({}, ind_to_var={})".format(
-            self.ind_to_obj, self.ind_to_var)
+        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
@@ -454,17 +457,25 @@ def _insert(self, 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):
-            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_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):
-            del self.ind_to_var[i]
+            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
@@ -482,6 +493,7 @@ def insert_equiv(self, *objs):
         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,)
@@ -492,6 +504,9 @@ def insert_equiv(self, *objs):
                         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
@@ -500,6 +515,15 @@ def insert_equiv(self, *objs):
                 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
+
         for i in range(ndim):
             names = [obj_name[i] for obj_name in obj_names]
             super(ShapeEquivSet, self).insert_equiv(*names)
@@ -525,8 +549,12 @@ def _get_shape(self, name):
         for i in inds:
             require(i in self.ind_to_var)
             vs = self.ind_to_var[i]
-            require(vs != [])
-            shape.append(vs[0])
+            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):
@@ -1101,7 +1129,11 @@ def _analyze_inst(self, label, scope, equiv_set, inst, redefined):
                 return pre, post
 
             if isinstance(typ, types.ArrayCompatible):
-                if (shape == None or isinstance(shape, tuple) or
+                if (shape is not None and
+                    isinstance(shape, ir.Var) and
+                    isinstance(self.typemap[shape.name], types.containers.Tuple)):
+                    pass
+                elif (shape == None or isinstance(shape, tuple) or
                     (isinstance(shape, ir.Var) and
                      not equiv_set.has_shape(shape))):
                     (shape, post) = self._gen_shape_call(equiv_set, lhs,


Test patch:
diff --git a/numba/tests/test_parfors.py b/numba/tests/test_parfors.py
--- a/numba/tests/test_parfors.py
+++ b/numba/tests/test_parfors.py
@@ -54,6 +54,9 @@
 linux_only = unittest.skipIf(not sys.platform.startswith('linux'), _lnx_reason)
 x86_only = unittest.skipIf(platform.machine() not in ('i386', 'x86_64'), 'x86 only test')
 
+_GLOBAL_INT_FOR_TESTING1 = 17
+_GLOBAL_INT_FOR_TESTING2 = 5
+
 class TestParforsBase(TestCase):
     """
     Base class for testing parfors.
@@ -877,6 +880,22 @@ def test_impl(A):
         self.assertTrue(countParfors(test_impl, (types.Array(types.float64, 1, 'C'), )) == 2)
         self.assertTrue(countParfors(test_impl, (types.Array(types.float64, 2, 'C'), )) == 2)
 
+    @skip_unsupported
+    def test_issue4963_globals(self):
+        def test_impl():
+            buf = np.zeros((_GLOBAL_INT_FOR_TESTING1, _GLOBAL_INT_FOR_TESTING2))
+            return buf
+        self.check(test_impl)
+
+    @skip_unsupported
+    def test_issue4963_freevars(self):
+        _FREEVAR_INT_FOR_TESTING1 = 17
+        _FREEVAR_INT_FOR_TESTING2 = 5
+        def test_impl():
+            buf = np.zeros((_FREEVAR_INT_FOR_TESTING1, _FREEVAR_INT_FOR_TESTING2))
+            return buf
+        self.check(test_impl)
+
     @skip_unsupported
     def test_random_parfor(self):
         """