Repository: numba/numba

Issue:
Function with two logic paths and two pranges gives erroneous result when compiled with parallel=True
<!--

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).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [ x ] I have tried 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 a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

Minimal reproducing code sample:
```
def proof(flag):
    result = 0
    if flag:
        for i in numba.prange(1000000):
            result += 1
    else:
        for i in numba.prange(1000000):
            result -= 3
    return result
```

I expect this function to return the same result with flag set to False whether compiled with our without parallelism.
Results:
```
Python 3.8.12 (default, Dec  6 2021, 03:13:14) 
[GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import numpy
>>> import numba
>>> numpy.__version__
'1.20.3'
>>> numba.__version__
'0.54.1'
>>> def proof(flag):
...     result = 0
...     if flag:
...         for i in numba.prange(1000000):
...             result += 1
...     else:
...         for i in numba.prange(1000000):
...             result -= 3
...     return result
... 
>>> good = numba.njit(parallel=False)(proof)
>>> bad  = numba.njit(parallel=True)(proof)
>>> good(True)   #correct
1000000
>>> good(False)  #correct
-3000000
>>> bad(True)  # this is ok
1000000
>>> bad(False)  # fails here
0
```


Discussion:
@erdman thank you for reporting this. Here is a better reproducer (one that can be copied and pasted):


```
import numba

def proof(flag):
    result = 0
    if flag:
        for i in numba.prange(1000000):
            result += 1
    else:
        for i in numba.prange(1000000):
            result -= 3
    return result


good = numba.njit(parallel=False)(proof)
bad  = numba.njit(parallel=True)(proof)

print(good(True))   #correct
print(good(False))  #correct
print(bad(True))  # this is ok
print(bad(False))  # fails here
```

Results:

```
1000000
-3000000
OMP: Info #271: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.
1000000
0
```


My hunch would be that a mutex/lock around the variable is missing somewhere? Or maybe `result` is typed as unsigned? But those are just hunches. @DrTodd13 could you take a look perhaps?

Resolving patch:
diff --git a/numba/parfors/parfor.py b/numba/parfors/parfor.py
--- a/numba/parfors/parfor.py
+++ b/numba/parfors/parfor.py
@@ -2568,7 +2568,13 @@ def find_mask_from_size(size_var):
                                     ("prange", loop_kind, loop_replacing),
                                     pass_states.flags, races=races)
 
-                    blocks[loop.header].body = [parfor, ir.Jump(list(loop.exits)[0], loc)]
+                    blocks[loop.header].body = [parfor]
+                    # We have to insert the header_body after the parfor because in
+                    # a Numba loop this will be executed one more times before the
+                    # branch and may contain instructions such as variable renamings
+                    # that are relied upon later.
+                    blocks[loop.header].body.extend(header_body)
+                    blocks[loop.header].body.append(ir.Jump(list(loop.exits)[0], loc))
                     self.rewritten.append(dict(
                         old_loop=loop,
                         new=parfor,


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
@@ -3524,7 +3524,7 @@ def test_impl(a, b, c):
                            scheduler_type='unsigned',
                            check_fastmath=True)
 
-    def test_prange_28(self):
+    def test_prange28(self):
         # issue7105: label conflict in nested parfor
         def test_impl(x, y):
             out = np.zeros(len(y))
@@ -3554,6 +3554,21 @@ def test_impl(x, y):
         self.prange_tester(test_impl, X, Y, scheduler_type='unsigned',
                            check_fastmath=True, check_fastmath_result=True)
 
+    def test_prange29(self):
+        # issue7630: SSA renaming in prange header
+        def test_impl(flag):
+            result = 0
+            if flag:
+                for i in range(1):
+                    result += 1
+            else:
+                for i in range(1):
+                    result -= 3
+            return result
+
+        self.prange_tester(test_impl, True)
+        self.prange_tester(test_impl, False)
+
 
 @skip_parfors_unsupported
 class TestPrangeSpecific(TestPrangeBase):