Repository: numba/numba

Issue:
[Python 3.12] [Task] Parfor test failures due to Python 3.12 bytecode changes. 
Due to Python 3.12 changes the parfor test: `test_parfors.py::TestPrangeSpecific::test_prange_two_instances_same_reduction_var` and `test_parfors.py::TestPrangeSpecific::test_prange_conflicting_reduction_ops` now fail with similar error (which relates to reduction variable within parfors). The fix for this could not be accommodated in https://github.com/numba/numba/pull/9246 (Python 3.12 support update PR) due to technical constraints. 

This issue now tracks the respective test failures and discussions pertaining to it's possible solution. 


Discussion:
@DrTodd13 now that the Python 3.12 support branch has been merged, please could you take a look at the above tests which are failing when run on Python 3.12? Both seems to be analysis issues relating to parallel reductions. Many thanks!
Will look this week.
> Will look this week.

Thanks @DrTodd13!

Resolving patch:
diff --git a/numba/parfors/parfor.py b/numba/parfors/parfor.py
--- a/numba/parfors/parfor.py
+++ b/numba/parfors/parfor.py
@@ -3874,6 +3874,43 @@ def lookup(var, varonly=True, start=None):
                 # e.g. $2 = a + $1; a = $2
                 # reductions that are functions calls like max() don't have an
                 # extra assignment afterwards
+
+                # This code was created when Numba had an IR generation strategy
+                # where a binop for a reduction would be followed by an
+                # assignment as follows:
+                #$c.4.15 = inplace_binop(fn=<iadd>, ...>, lhs=c.3, rhs=$const20)
+                #c.4 = $c.4.15
+
+                # With Python 3.12 changes, Numba may separate that assignment
+                # to a new basic block.  The code below looks and sees if an
+                # assignment to the reduction var follows the reduction operator
+                # and if not it searches the rest of the reduction nodes to find
+                # the assignment that should follow the reduction operator
+                # and then reorders the reduction nodes so that assignment
+                # follows the reduction operator.
+                if (i + 1 < len(nodes) and
+                    ((not isinstance(nodes[i + 1], ir.Assign)) or
+                     nodes[i + 1].target.unversioned_name != unversioned_name)):
+                    foundj = None
+                    # Iterate through the rest of the reduction nodes.
+                    for j, jstmt in enumerate(nodes[i + 1:]):
+                        # If this stmt is an assignment where the right-hand
+                        # side of the assignment is the output of the reduction
+                        # operator.
+                        if isinstance(jstmt, ir.Assign) and jstmt.value == lhs:
+                            # Remember the index of this node.  Because of
+                            # nodes[i+1] above, we have to add i + 1 to j below
+                            # to get the index in the original nodes list.
+                            foundj = i + j + 1
+                            break
+                    if foundj is not None:
+                        # If we found the correct assignment then move it to
+                        # after the reduction operator.
+                        nodes = (nodes[:i + 1] +   # nodes up to operator
+                                 nodes[foundj:foundj + 1] + # assignment node
+                                 nodes[i + 1:foundj] + # between op and assign
+                                 nodes[foundj + 1:]) # after assignment node
+
                 if (not (i+1 < len(nodes) and isinstance(nodes[i+1], ir.Assign)
                         and nodes[i+1].target.unversioned_name == unversioned_name)
                         and lhs.unversioned_name != unversioned_name):


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
@@ -46,7 +46,7 @@
                       override_env_config, linux_only, tag,
                       skip_parfors_unsupported, _32bit, needs_blas,
                       needs_lapack, disabled_test, skip_unless_scipy,
-                      needs_subprocess, expected_failure_py312)
+                      needs_subprocess)
 from numba.core.extending import register_jitable
 from numba.core.bytecode import _fix_LOAD_GLOBAL_arg
 from numba.core import utils
@@ -4002,7 +4002,6 @@ def test_impl(x, par, numthreads):
 class TestPrangeSpecific(TestPrangeBase):
     """ Tests specific features/problems found under prange"""
 
-    @expected_failure_py312
     def test_prange_two_instances_same_reduction_var(self):
         # issue4922 - multiple uses of same reduction variable
         def test_impl(n):
@@ -4014,7 +4013,6 @@ def test_impl(n):
             return c
         self.prange_tester(test_impl, 9)
 
-    @expected_failure_py312
     def test_prange_conflicting_reduction_ops(self):
         def test_impl(n):
             c = 0