Repository: numba/numba

Issue:
Parfors reordering print statement in fusion.
## Reporting a bug

- [X] I am using ~the latest released~ mainline 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).

This:
```python
import numpy as np
from numba import njit

@njit
def nop():
    pass

def foo():
    a = np.zeros(10)
    a[1:8] = np.arange(0, 7)
    #nop()
    print('a[3]:', a[3])
    print('a[3]:', a[3])
    return a

f = njit(foo)

pd = {'comprehension': True,  # parallel comprehension
  'prange':        True,  # parallel for-loop
  'numpy':         True,  # parallel numpy calls
  'reduction':     True,  # parallel reduce calls
  'setitem':       True,  # parallel setitem
  'stencil':       True,  # parallel stencils
  'fusion':        True,  # enable fusion or not
}

fn = njit(parallel=pd)(foo)
print(fn())
```
produces:
```
a[3]: 0.0
a[3]: 2.0
[0. 0. 1. 2. 3. 4. 5. 6. 0. 0.]
```
Switching off fusion, or uncommenting the `nop` function call, results in the correct answer. Seems like the `print` call is being reordered when it should not be. Whilst print could be considered side-effect free, it's alarming if it gets moved!


Discussion:
Having looked at the IR a bit, the print seems to remain in the correct location and it's the `static_getitem` that gets moved. The reason for the move is that in `_can_reorder_stmts`, `get_stmt_writes` and `get_parfor_writes` considers only a direct write without considering potential aliases.

This code is also problematic, but I think for a different reason (produces, 2, 2, 0).
```python
import numpy as np
from numba import njit

def foo():
    a = np.zeros(10)
    a[1:8] = np.arange(0, 7)
    y = a[3]
    return y

f = njit()(foo)
fp = njit(parallel=True)(foo)

print(foo())
print(f())
print(fp())
```

Resolving patch:
diff --git a/numba/parfor.py b/numba/parfor.py
--- a/numba/parfor.py
+++ b/numba/parfor.py
@@ -76,7 +76,8 @@
     is_setitem,
     is_get_setitem,
     index_var_of_get_setitem,
-    set_index_var_of_get_setitem)
+    set_index_var_of_get_setitem,
+    find_potential_aliases)
 
 from numba.analysis import (compute_use_defs, compute_live_map,
                             compute_dead_maps, compute_cfg_from_blocks)
@@ -1464,12 +1465,12 @@ def run(self):
             self.array_analysis.run(self.func_ir.blocks)
             # reorder statements to maximize fusion
             # push non-parfors down
-            maximize_fusion(self.func_ir, self.func_ir.blocks,
+            maximize_fusion(self.func_ir, self.func_ir.blocks, self.typemap,
                                                             up_direction=False)
             dprint_func_ir(self.func_ir, "after maximize fusion down")
             self.fuse_parfors(self.array_analysis, self.func_ir.blocks)
             # push non-parfors up
-            maximize_fusion(self.func_ir, self.func_ir.blocks)
+            maximize_fusion(self.func_ir, self.func_ir.blocks, self.typemap)
             dprint_func_ir(self.func_ir, "after maximize fusion up")
             # try fuse again after maximize
             self.fuse_parfors(self.array_analysis, self.func_ir.blocks)
@@ -2435,7 +2436,7 @@ def fuse_parfors(self, array_analysis, blocks):
 
     def fuse_recursive_parfor(self, parfor, equiv_set):
         blocks = wrap_parfor_blocks(parfor)
-        maximize_fusion(self.func_ir, blocks)
+        maximize_fusion(self.func_ir, blocks, self.typemap)
         arr_analysis = array_analysis.ArrayAnalysis(self.typingctx, self.func_ir,
                                                 self.typemap, self.calltypes)
         arr_analysis.run(blocks, equiv_set)
@@ -3159,19 +3160,20 @@ def __init__(self, blocks):
 postproc.ir_extension_insert_dels[Parfor] = parfor_insert_dels
 
 
-def maximize_fusion(func_ir, blocks, up_direction=True):
+def maximize_fusion(func_ir, blocks, typemap, up_direction=True):
     """
     Reorder statements to maximize parfor fusion. Push all parfors up or down
     so they are adjacent.
     """
     call_table, _ = get_call_table(blocks)
+    alias_map, arg_aliases = find_potential_aliases(blocks, func_ir.arg_names, typemap, func_ir)
     for block in blocks.values():
         order_changed = True
         while order_changed:
-            order_changed = maximize_fusion_inner(func_ir, block,
-                                                    call_table, up_direction)
+            order_changed = maximize_fusion_inner(func_ir, block, call_table,
+                                                  alias_map, up_direction)
 
-def maximize_fusion_inner(func_ir, block, call_table, up_direction=True):
+def maximize_fusion_inner(func_ir, block, call_table, alias_map, up_direction=True):
     order_changed = False
     i = 0
     # i goes to body[-3] (i+1 to body[-2]) since body[-1] is terminator and
@@ -3179,9 +3181,9 @@ def maximize_fusion_inner(func_ir, block, call_table, up_direction=True):
     while i < len(block.body) - 2:
         stmt = block.body[i]
         next_stmt = block.body[i+1]
-        can_reorder = (_can_reorder_stmts(stmt, next_stmt, func_ir, call_table)
+        can_reorder = (_can_reorder_stmts(stmt, next_stmt, func_ir, call_table, alias_map)
                         if up_direction else _can_reorder_stmts(next_stmt, stmt,
-                        func_ir, call_table))
+                        func_ir, call_table, alias_map))
         if can_reorder:
             block.body[i] = next_stmt
             block.body[i+1] = stmt
@@ -3189,7 +3191,16 @@ def maximize_fusion_inner(func_ir, block, call_table, up_direction=True):
         i += 1
     return order_changed
 
-def _can_reorder_stmts(stmt, next_stmt, func_ir, call_table):
+def expand_aliases(the_set, alias_map):
+    ret = set()
+    for i in the_set:
+        if i in alias_map:
+            ret = ret.union(alias_map[i])
+        else:
+            ret.add(i)
+    return ret
+
+def _can_reorder_stmts(stmt, next_stmt, func_ir, call_table, alias_map):
     """
     Check dependencies to determine if a parfor can be reordered in the IR block
     with a non-parfor statement.
@@ -3206,10 +3217,10 @@ def _can_reorder_stmts(stmt, next_stmt, func_ir, call_table):
                  or has_no_side_effect(
                 next_stmt.value, set(), call_table)
             or guard(is_assert_equiv, func_ir, next_stmt.value))):
-        stmt_accesses = {v.name for v in stmt.list_vars()}
-        stmt_writes = get_parfor_writes(stmt)
-        next_accesses = {v.name for v in next_stmt.list_vars()}
-        next_writes = get_stmt_writes(next_stmt)
+        stmt_accesses = expand_aliases({v.name for v in stmt.list_vars()}, alias_map)
+        stmt_writes = expand_aliases(get_parfor_writes(stmt), alias_map)
+        next_accesses = expand_aliases({v.name for v in next_stmt.list_vars()}, alias_map)
+        next_writes = expand_aliases(get_stmt_writes(next_stmt), alias_map)
         if len((stmt_writes & next_accesses)
                 | (next_writes & stmt_accesses)) == 0:
             return True


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
@@ -1207,7 +1207,7 @@ def test_impl(out):
 
         out = np.ones(1)
         self.check(test_impl, out)
-        
+
     @skip_unsupported
     @needs_blas
     def test_parfor_generate_fuse(self):
@@ -1274,10 +1274,10 @@ def test_impl(n):
 
             for i in numba.prange(n):
                 result1 += tmp
-                
+
             for i in numba.prange(n):
                 result1 += tmp
-                
+
             return result1
 
         self.check(test_impl, 100)
@@ -2415,6 +2415,16 @@ def test_impl(m, A):
 
         self.check(test_impl, 10, np.ones(10))
 
+    @skip_unsupported
+    def test_parfor_slice18(self):
+        # issue 3534
+        def test_impl():
+            a = np.zeros(10)
+            a[1:8] = np.arange(0, 7)
+            y = a[3]
+            return y
+
+        self.check(test_impl)
 
 class TestParforsOptions(TestParforsBase):
 
@@ -2528,7 +2538,7 @@ def test_impl(a, b, c):
             self.check(test_impl, a, b, c)
         self.assertIn("\'@do_scheduling\' not found", str(raises.exception))
 
-class TestParforsMisc(TestCase):
+class TestParforsMisc(TestParforsBase):
     """
     Tests miscellaneous parts of ParallelAccelerator use.
     """
@@ -2559,6 +2569,21 @@ def pyfunc():
         # make sure the cache is set to false, cf. NullCache
         self.assertTrue(isinstance(cfunc._cache, numba.caching.NullCache))
 
+    @skip_unsupported
+    def test_statement_reordering_respects_aliasing(self):
+        def impl():
+            a = np.zeros(10)
+            a[1:8] = np.arange(0, 7)
+            print('a[3]:', a[3])
+            print('a[3]:', a[3])
+            return a
+
+        cres = self.compile_parallel(impl, ())
+        with captured_stdout() as stdout:
+            cres.entry_point()
+        for line in stdout.getvalue().splitlines():
+            self.assertEqual('a[3]: 2.0', line)
+
 
 @skip_unsupported
 class TestParforsDiagnostics(TestParforsBase):