Repository: numba/numba

Issue:
Incorrect results from (deterministic) prange loop
<!--

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 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).


Please include details of the bug here, including, if applicable, what you
expected to happen!

-->
This seems sufficiently different from https://github.com/numba/numba/issues/5942 that I'm bringing it up as a separate issue.

First, a case where the array doesn't seem to be read correctly:
```
import numba as nb
@nb.njit(parallel=True)
def f():
    for k in nb.prange(5):
        X = [0]
        X[0] = 1
        print(k, X, X[0])
```

Actual:
```
>>> f()
4 [1] 0
0 [1] 0
3 [1] 0
2 [1] 0
1 [1] 0
```
Expected: lines of the form `k [1] 1`

Next a case where the array doesn't seem to be written to correctly.
```
import numba as nb
@nb.njit(parallel=True)
def f(n):
    for k in nb.prange(n):
        X = [0]
        print(k, X)
        for j in range(1):
            X[j] = 1
```
It seems that in each thread the array X doesn't get re-initialized to [0].
<details>

```
>>> f(2*8+1)  # my machine has 8 threads.
0 [0]
2 [0]
3 [1]
8 [0]
9 [1]
14 [0]
4 [0]
1 [1]
12 [0]
15 [1]
6 [0]
5 [1]
10 [0]
13 [1]
7 [1]
11 [1]
16 [1]
>>> 
```
The order of the lines will change but the values do not.
</details>


Discussion:
Thanks for the report. This is a bug, and probably the same bug in both cases.

Case 1:
```python
import numba as nb
@nb.njit(parallel=True)
def f():
    for k in nb.prange(5):
        X = [0]
        X[0] = 1
        print(k, X, X[0])

f()
```
I think what's happening is that the `build_list` of `X = [0]` is being hoisted as loop invariant when it ought not be.

Case 2:
```python
import numba as nb
@nb.njit(parallel=True)
def f(n):
    for k in nb.prange(n):
        X = [0]
        print(k, X)
        for j in range(1):
            X[j] = 1

f(3)
```
same as Case 1, think `X = [0]` is erroneously hoisted as loop invariant.

CC @DrTodd13 

Resolving patch:
diff --git a/numba/parfors/parfor_lowering.py b/numba/parfors/parfor_lowering.py
--- a/numba/parfors/parfor_lowering.py
+++ b/numba/parfors/parfor_lowering.py
@@ -677,7 +677,7 @@ def compute_def_once(loop_body, typemap):
     getattr_taken = {}
     module_assigns = {}
     compute_def_once_internal(loop_body, def_once, def_more, getattr_taken, typemap, module_assigns)
-    return def_once
+    return def_once, def_more
 
 def find_vars(var, varset):
     assert isinstance(var, ir.Var)
@@ -738,13 +738,25 @@ def find_setitems_body(setitems, itemsset, loop_body, typemap):
     for label, block in loop_body.items():
         find_setitems_block(setitems, itemsset, block, typemap)
 
+def empty_container_allocator_hoist(inst, dep_on_param, call_table, hoisted,
+                                    not_hoisted, typemap, stored_arrays):
+    if (isinstance(inst, ir.Assign) and
+        isinstance(inst.value, ir.Expr) and
+        inst.value.op == 'call' and
+        inst.value.func.name in call_table):
+        call_list = call_table[inst.value.func.name]
+        if call_list == ['empty', np]:
+            return _hoist_internal(inst, dep_on_param, call_table, hoisted,
+                                   not_hoisted, typemap, stored_arrays)
+    return False
+
 def hoist(parfor_params, loop_body, typemap, wrapped_blocks):
     dep_on_param = copy.copy(parfor_params)
     hoisted = []
     not_hoisted = []
 
     # Compute the set of variable defined exactly once in the loop body.
-    def_once = compute_def_once(loop_body, typemap)
+    def_once, def_more = compute_def_once(loop_body, typemap)
     (call_table, reverse_call_table) = get_call_table(wrapped_blocks)
 
     setitems = set()
@@ -753,11 +765,16 @@ def hoist(parfor_params, loop_body, typemap, wrapped_blocks):
     dep_on_param = list(set(dep_on_param).difference(setitems))
     if config.DEBUG_ARRAY_OPT >= 1:
         print("hoist - def_once:", def_once, "setitems:", setitems, "itemsset:", itemsset, "dep_on_param:", dep_on_param, "parfor_params:", parfor_params)
+    for si in setitems:
+        add_to_def_once_sets(si, def_once, def_more)
 
     for label, block in loop_body.items():
         new_block = []
         for inst in block.body:
-            if isinstance(inst, ir.Assign) and inst.target.name in def_once:
+            if empty_container_allocator_hoist(inst, dep_on_param, call_table,
+                                   hoisted, not_hoisted, typemap, itemsset):
+                continue
+            elif isinstance(inst, ir.Assign) and inst.target.name in def_once:
                 if _hoist_internal(inst, dep_on_param, call_table,
                                    hoisted, not_hoisted, typemap, itemsset):
                     # don't add this instruction to the block since it is
@@ -769,11 +786,14 @@ def hoist(parfor_params, loop_body, typemap, wrapped_blocks):
                     print("parfor")
                     inst.dump()
                 for ib_inst in inst.init_block.body:
-                    if (isinstance(ib_inst, ir.Assign) and
+                    if empty_container_allocator_hoist(ib_inst, dep_on_param,
+                        call_table, hoisted, not_hoisted, typemap, itemsset):
+                        continue
+                    elif (isinstance(ib_inst, ir.Assign) and
                         ib_inst.target.name in def_once):
                         if _hoist_internal(ib_inst, dep_on_param, call_table,
                                            hoisted, not_hoisted, typemap, itemsset):
-                            # don't add this instuction to the block since it is hoisted
+                            # don't add this instruction to the block since it is hoisted
                             continue
                     new_init_block.append(ib_inst)
                 inst.init_block.body = new_init_block


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
@@ -2540,6 +2540,21 @@ def test_impl(image, a, b):
         image = np.zeros((3, 3), dtype=np.int32)
         self.prange_tester(test_impl, image, 0, 0)
 
+    @skip_parfors_unsupported
+    def test_list_setitem_hoisting(self):
+        # issue5979
+        # Don't hoist list initialization if list item set.
+        def test_impl():
+            n = 5
+            a = np.empty(n, dtype=np.int64)
+            for k in range(5):
+                X = [0]
+                X[0] = 1
+                a[k] = X[0]
+            return a
+
+        self.prange_tester(test_impl)
+
 
 @skip_parfors_unsupported
 @x86_only