Repository: numba/numba

Issue:
Code hang bug on py36.  Maybe parfor related.
## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

The following hangs with threads usage max'ed out on osx, py3.6 on 9dfaa295cb43d43023add5254d5768651877787d and 0.40.0 released version.

Code:

```python
import numpy as np
from numba import njit, prange


@njit(parallel=True)
def core(volcano, anglebreaks, volcanoshadow):
    for i in prange(0, volcano.shape[0]):
        for j in range(0, volcano.shape[1]):
            #### Looping using the following line will deadlock
            #### Also, turning off parallel=True makes it work too
            for anglei in anglebreaks:
            ### Loop using the following lines will work
            # for m in range(len(anglebreaks)):
            #     anglei = anglebreaks[m]
                volcanoshadow[i, j] = volcanoshadow[i, j] - 1


def bench_rays(volcano):
    volcanoshadow = np.ones_like(volcano)
    angle = -90. / 180. * np.pi
    diffangle = 90. / 180. * np.pi
    nangles = 25
    anglebreaks = np.tan(np.linspace(angle, diffangle, nangles, endpoint=True))

    core(volcano, anglebreaks, volcanoshadow)
    return volcanoshadow


if __name__ == '__main__':
    shape = 87, 61
    volcano = np.ones(shape)
    print(volcano.shape)
    shade = bench_rays(volcano)
    print(shade)
```

The code works fine on py3.5.  Py3.7 not tested.


Discussion:
Linux64 Py37 also has the same behaviour.
The hang is threading backend invariant so may be bad code gen.

Resolving patch:
diff --git a/numba/ir_utils.py b/numba/ir_utils.py
--- a/numba/ir_utils.py
+++ b/numba/ir_utils.py
@@ -637,19 +637,22 @@ def is_pure(rhs, lives, call_table):
         returns the same result.  This is not the case for things
         like calls to numpy.random.
     """
-    if isinstance(rhs, ir.Expr) and rhs.op == 'call':
-        func_name = rhs.func.name
-        if func_name not in call_table or call_table[func_name] == []:
-            return False
-        call_list = call_table[func_name]
-        if (call_list == [slice] or
-            call_list == ['log', numpy] or
-            call_list == ['empty', numpy]):
-            return True
-        for f in is_pure_extensions:
-            if f(rhs, lives, call_list):
+    if isinstance(rhs, ir.Expr):
+        if rhs.op == 'call':
+            func_name = rhs.func.name
+            if func_name not in call_table or call_table[func_name] == []:
+                return False
+            call_list = call_table[func_name]
+            if (call_list == [slice] or
+                call_list == ['log', numpy] or
+                call_list == ['empty', numpy]):
                 return True
-        return False
+            for f in is_pure_extensions:
+                if f(rhs, lives, call_list):
+                    return True
+            return False
+        elif rhs.op == 'getiter' or rhs.op == 'iternext':
+            return False
     if isinstance(rhs, ir.Yield):
         return False
     return True
diff --git a/numba/npyufunc/parfor.py b/numba/npyufunc/parfor.py
--- a/numba/npyufunc/parfor.py
+++ b/numba/npyufunc/parfor.py
@@ -472,7 +472,7 @@ def unwrap_loop_body(loop_body):
     last_label = max(loop_body.keys())
     loop_body[last_label].body = loop_body[last_label].body[:-1]
 
-def compute_def_once_block(block, def_once, def_more):
+def compute_def_once_block(block, def_once, def_more, getattr_taken):
     assignments = block.find_insts(ir.Assign)
     for one_assign in assignments:
         a_def = one_assign.target.name
@@ -484,18 +484,27 @@ def compute_def_once_block(block, def_once, def_more):
         else:
             def_once.add(a_def)
 
-def compute_def_once_internal(loop_body, def_once, def_more):
+        rhs = one_assign.value
+        if isinstance(rhs, ir.Expr) and rhs.op == 'getattr' and rhs.value.name in def_once:
+            getattr_taken[a_def] = rhs.value.name
+        if isinstance(rhs, ir.Expr) and rhs.op == 'call' and rhs.func.name in getattr_taken:
+            base_obj = getattr_taken[rhs.func.name]
+            def_more.add(base_obj)
+            def_once.remove(base_obj)
+
+def compute_def_once_internal(loop_body, def_once, def_more, getattr_taken):
     for label, block in loop_body.items():
-        compute_def_once_block(block, def_once, def_more)
+        compute_def_once_block(block, def_once, def_more, getattr_taken)
         for inst in block.body:
             if isinstance(inst, parfor.Parfor):
-                compute_def_once_block(inst.init_block, def_once, def_more)
-                compute_def_once_internal(inst.loop_body, def_once, def_more)
+                compute_def_once_block(inst.init_block, def_once, def_more, getattr_taken)
+                compute_def_once_internal(inst.loop_body, def_once, def_more, getattr_taken)
 
 def compute_def_once(loop_body):
     def_once = set()
     def_more = set()
-    compute_def_once_internal(loop_body, def_once, def_more)
+    getattr_taken = {}
+    compute_def_once_internal(loop_body, def_once, def_more, getattr_taken)
     return def_once
 
 def find_vars(var, varset):


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
@@ -1358,6 +1358,22 @@ def test_impl(A):
         A = np.arange(n)
         self.check(test_impl, A)
 
+    @skip_unsupported
+    def test_no_hoisting_with_member_function_call(self):
+        def test_impl(X):
+            n = X.shape[0]
+            acc = 0
+            for i in prange(n):
+                R = {1, 2, 3}
+                R.add(i)
+                tmp = 0
+                for x in R:
+                    tmp += x
+                acc += tmp
+            return acc
+
+        self.check(test_impl, np.random.ranf(128))
+
 
 class TestPrangeBase(TestParforsBase):