Repository: numba/numba

Issue:
BUG: AssertionError: Failed in nopython mode pipeline (step: convert to parfors). Invalid reduction format. (Error with parallel=True)
- [X] I am using numba 0.49.1 and numpy 1.18.4.
- [X] I have included a minimal working reproducer.

<!--

-->
This example works with parallel=False, but it does not with parallel=True. I simplified my code as far as I could to make it simple.

```
@jit(nopython=True, parallel=True)
def test(image):
    empty=numpy.zeros(image.shape)
    for i in prange(image.shape[0]):
        for j in prange(image.shape[1]):
            r = g = b = image[i][j][0]/255.0
            cmin = cmax = 0
            delta = cmax-cmin # parallel works if "delta = <number>" is in code
            if delta == 0:
                h = 0
            if cmax == r and delta != 0:
                h = 0
            empty[i][j] = [h, h, h] # parallel works if this is removed
    return empty

image = cv2.imread('rgb_image.png')
test(image)
```
Error:
```
AssertionError: Failed in nopython mode pipeline (step: convert to parfors)
Invalid reduction format
````

<details>
<summary>Full error message</summary>

```
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-11-de0d9d52ad45> in <module>
    151 
    152 
--> 153 print(test(image))
    154 
    155 

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\dispatcher.py in _compile_for_args(self, *args, **kws)
    418                     e.patch_message('\n'.join((str(e).rstrip(), help_msg)))
    419             # ignore the FULL_TRACEBACKS config, this needs reporting!
--> 420             raise e
    421 
    422     def inspect_llvm(self, signature=None):

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\dispatcher.py in _compile_for_args(self, *args, **kws)
    351                 argtypes.append(self.typeof_pyval(a))
    352         try:
--> 353             return self.compile(tuple(argtypes))
    354         except errors.ForceLiteralArg as e:
    355             # Received request for compiler re-entry with the list of arguments

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler_lock.py in _acquire_compile_lock(*args, **kwargs)
     30         def _acquire_compile_lock(*args, **kwargs):
     31             with self:
---> 32                 return func(*args, **kwargs)
     33         return _acquire_compile_lock
     34 

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\dispatcher.py in compile(self, sig)
    792             self._cache_misses[sig] += 1
    793             try:
--> 794                 cres = self._compiler.compile(args, return_type)
    795             except errors.ForceLiteralArg as e:
    796                 def folded(args, kws):

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\dispatcher.py in compile(self, args, return_type)
     75 
     76     def compile(self, args, return_type):
---> 77         status, retval = self._compile_cached(args, return_type)
     78         if status:
     79             return retval

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\dispatcher.py in _compile_cached(self, args, return_type)
     89 
     90         try:
---> 91             retval = self._compile_core(args, return_type)
     92         except errors.TypingError as e:
     93             self._failed_cache[key] = e

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\dispatcher.py in _compile_core(self, args, return_type)
    102 
    103         impl = self._get_implementation(args, {})
--> 104         cres = compiler.compile_extra(self.targetdescr.typing_context,
    105                                       self.targetdescr.target_context,
    106                                       impl,

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler.py in compile_extra(typingctx, targetctx, func, args, return_type, flags, locals, library, pipeline_class)
    566     pipeline = pipeline_class(typingctx, targetctx, library,
    567                               args, return_type, flags, locals)
--> 568     return pipeline.compile_extra(func)
    569 
    570 

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler.py in compile_extra(self, func)
    337         self.state.lifted = ()
    338         self.state.lifted_from = None
--> 339         return self._compile_bytecode()
    340 
    341     def compile_ir(self, func_ir, lifted=(), lifted_from=None):

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler.py in _compile_bytecode(self)
    399         """
    400         assert self.state.func_ir is None
--> 401         return self._compile_core()
    402 
    403     def _compile_ir(self):

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler.py in _compile_core(self)
    379                 self.state.status.fail_reason = e
    380                 if is_final_pipeline:
--> 381                     raise e
    382         else:
    383             raise CompilerError("All available pipelines exhausted")

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler.py in _compile_core(self)
    370             res = None
    371             try:
--> 372                 pm.run(self.state)
    373                 if self.state.cr is not None:
    374                     break

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler_machinery.py in run(self, state)
    339                     (self.pipeline_name, pass_desc)
    340                 patched_exception = self._patch_error(msg, e)
--> 341                 raise patched_exception
    342 
    343     def dependency_analysis(self):

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler_machinery.py in run(self, state)
    330                 pass_inst = _pass_registry.get(pss).pass_inst
    331                 if isinstance(pass_inst, CompilerPass):
--> 332                     self._runPass(idx, pass_inst, state)
    333                 else:
    334                     raise BaseException("Legacy pass in use")

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler_lock.py in _acquire_compile_lock(*args, **kwargs)
     30         def _acquire_compile_lock(*args, **kwargs):
     31             with self:
---> 32                 return func(*args, **kwargs)
     33         return _acquire_compile_lock
     34 

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler_machinery.py in _runPass(self, index, pss, internal_state)
    289             mutated |= check(pss.run_initialization, internal_state)
    290         with SimpleTimer() as pass_time:
--> 291             mutated |= check(pss.run_pass, internal_state)
    292         with SimpleTimer() as finalize_time:
    293             mutated |= check(pss.run_finalizer, internal_state)

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\compiler_machinery.py in check(func, compiler_state)
    262 
    263         def check(func, compiler_state):
--> 264             mangled = func(compiler_state)
    265             if mangled not in (True, False):
    266                 msg = ("CompilerPass implementations should return True/False. "

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\core\typed_passes.py in run_pass(self, state)
    286                                          state.flags,
    287                                          state.parfor_diagnostics)
--> 288         parfor_pass.run()
    289 
    290         remove_dels(state.func_ir.blocks)

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\parfors\parfor.py in run(self)
   2692             # Validate reduction in parfors.
   2693             for p in parfors:
-> 2694                 get_parfor_reductions(self.func_ir, p, p.params, self.calltypes)
   2695 
   2696             # Validate parameters:

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\parfors\parfor.py in get_parfor_reductions(func_ir, parfor, parfor_params, calltypes, reductions, reduce_varnames, param_uses, param_nodes, var_to_param)
   3316             reduce_varnames.append(param_name)
   3317             param_nodes[param].reverse()
-> 3318             reduce_nodes = get_reduce_nodes(param, param_nodes[param], func_ir)
   3319             check_conflicting_reduction_operators(param, reduce_nodes)
   3320             gri_out = guard(get_reduction_init, reduce_nodes)

c:\users\user\anaconda3\envs\folder\lib\site-packages\numba\parfors\parfor.py in get_reduce_nodes(reduction_node, nodes, func_ir)
   3422                 reduce_nodes = nodes[i:]
   3423                 break;
-> 3424     assert reduce_nodes, "Invalid reduction format"
   3425     return reduce_nodes
   3426 

AssertionError: Failed in nopython mode pipeline (step: convert to parfors)
Invalid reduction format
```
</details>


Discussion:
With some modifications to the reproducer, I can reproduce the error message:

```python
from numba import jit, prange
import numpy


@jit(nopython=True, parallel=True)
def test(image):
    empty = numpy.zeros(image.shape)
    for i in prange(image.shape[0]):
        for j in prange(image.shape[1]):
            r = g = b = image[i][j][0] / 255.0
            cmin = cmax = 0
            delta = cmax - cmin  # parallel works if "delta = <number>" is in code
            if delta == 0:
                h = 0
            if cmax == r and delta != 0:
                h = 0
            empty[i][j] = [h, h, h]  # parallel works if this is removed
    return empty


image = numpy.zeros((640, 480, 3), dtype=numpy.int32)
test(image)
```
I can't see any obvious use of unsupported functionality here - @DrTodd13 any thoughts on this?

Resolving patch:
diff --git a/numba/parfors/parfor.py b/numba/parfors/parfor.py
--- a/numba/parfors/parfor.py
+++ b/numba/parfors/parfor.py
@@ -3313,17 +3313,24 @@ def get_parfor_reductions(func_ir, parfor, parfor_params, calltypes, reductions=
         # param already
         param_name = param.name
         if param_name in used_vars and param_name not in reduce_varnames:
-            reduce_varnames.append(param_name)
             param_nodes[param].reverse()
             reduce_nodes = get_reduce_nodes(param, param_nodes[param], func_ir)
-            check_conflicting_reduction_operators(param, reduce_nodes)
-            gri_out = guard(get_reduction_init, reduce_nodes)
-            if gri_out is not None:
-                init_val, redop = gri_out
-            else:
-                init_val = None
-                redop = None
-            reductions[param_name] = (init_val, reduce_nodes, redop)
+            # Certain kinds of ill-formed Python (like potentially undefined
+            # variables) in combination with SSA can make things look like
+            # reductions except that they don't have reduction operators.
+            # If we get to this point but don't find a reduction operator
+            # then assume it is this situation and just don't treat this
+            # variable as a reduction.
+            if reduce_nodes is not None:
+                reduce_varnames.append(param_name)
+                check_conflicting_reduction_operators(param, reduce_nodes)
+                gri_out = guard(get_reduction_init, reduce_nodes)
+                if gri_out is not None:
+                    init_val, redop = gri_out
+                else:
+                    init_val = None
+                    redop = None
+                reductions[param_name] = (init_val, reduce_nodes, redop)
 
     return reduce_varnames, reductions
 
@@ -3421,7 +3428,6 @@ def lookup(var, varonly=True):
                 replace_vars_inner(rhs, replace_dict)
                 reduce_nodes = nodes[i:]
                 break;
-    assert reduce_nodes, "Invalid reduction format"
     return reduce_nodes
 
 def get_expr_args(expr):


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
@@ -2495,6 +2495,28 @@ def test_impl(x):
         x = [np.array([1,2,3], dtype=int),np.array([1,2], dtype=int)]
         self.prange_tester(test_impl, x)
 
+    @skip_parfors_unsupported
+    def test_ssa_false_reduction(self):
+        # issue5698
+        # SSA for h creates assignments to h that make it look like a
+        # reduction variable except that it lacks an associated
+        # reduction operator.  Test here that h is excluded as a
+        # reduction variable.
+        def test_impl(image, a, b):
+            empty = np.zeros(image.shape)
+            for i in range(image.shape[0]):
+                r = image[i][0] / 255.0
+                if a == 0:
+                    h = 0
+                if b == 0:
+                    h = 0
+                empty[i] = [h, h, h]
+            return empty
+
+        image = np.zeros((3, 3), dtype=np.int32)
+        self.prange_tester(test_impl, image, 0, 0)
+
+
 @skip_parfors_unsupported
 @x86_only
 class TestParforsVectorizer(TestPrangeBase):