Repository: numba/numba

Issue:
Reproducible bug in parallel mode when prange loop contains a condition on an optional argument
numba version 0.55.2
python 3.9.13
linux debian

When running parallel mode and the prange contains a condition on an optional argument the compilation fails.

**This fails:**

@nb.njit("f8[:](f8[:], optional(u1))", nogil=True, cache=False, parallel=True, fastmath=False) 
def test(arr, test=None):
    for i in numba.prange(10):
        if test is not None:
            arr[i] = 0.0
    return arr

**This works:**

@nb.njit("f8[:](f8[:], optional(u1))", nogil=True, cache=False, parallel=True, fastmath=False) 
def test(arr, test=None):
    for i in numba.prange(10):
            arr[i] = 0.0
    return arr


**Error:**

numba.core.errors.LoweringError: Failed in nopython mode pipeline (step: native lowering)
cannot store {i8, i1} to {i8, i8}*: mismatching types

File "realizedMeasures.py", line 134:
def test(arr, ft=None):
    for i in nb.prange(10):
    ^

During: lowering "id=0[LoopNest(index_variable = parfor_index.2, range = (0, $const6.2, 1))]


Discussion:
Confirmed.
Thanks for confirming @apmasell.

This is a reproducer:

```python
from numba import njit, prange
import numpy as np

@njit(parallel=True)
def foo(arr, pred=None):
    for i in prange(1):
        if pred is not None:
            arr[i] = 0.0
    return arr


@njit
def call_foo(opt_pred):
    a = np.ones(5)
    pred = None
    if opt_pred:
        pred = np.uint8(1)
    foo(a, pred)

call_foo(True)
```
or alternatively:
```python
from numba import njit, prange
import numpy as np

@njit("f8[:](f8[:], optional(u1))", parallel=True)
def foo(arr, pred=None):
    for i in prange(1):
        if pred is not None:
            arr[i] = 0.0
    return arr
```

CC @DrTodd13, I think the issue here is that parfors lowering is trying to store the value of an optional type into the first element of a size one array for the purposes of passing a scalar through to the underlying gufunc. This ends up as a type mismatch in the LLVM IR as the optional isn't unpacked. What do you think of something like this as a fix?
```diff
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
@@ -1807,8 +1807,14 @@ def call_parallel_gufunc(lowerer, cres, gu_signature, outer_sig, expr_args, expr
         else:
             if i < num_inps:
                 # Scalar input, need to store the value in an array of size 1
-                typ = (context.get_data_type(aty)
-                       if not isinstance(aty, types.Boolean)
+                if isinstance(aty, types.Optional):
+                    # Unpack optional type
+                    unpacked_aty = aty.type
+                    arg = context.cast(builder, arg, aty, unpacked_aty)
+                else:
+                    unpacked_aty = aty
+                typ = (context.get_data_type(unpacked_aty)
+                       if not isinstance(unpacked_aty, types.Boolean)
                        else llvmlite.ir.IntType(1))
                 ptr = cgutils.alloca_once(builder, typ)
                 builder.store(arg, ptr)
```
it unpacks the type ahead of the store.
@stuartarchibald @apmasell Seems fine to me.
> @stuartarchibald @apmasell Seems fine to me.

Great, thanks for taking a look @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
@@ -1807,8 +1807,14 @@ def load_potential_tuple_var(x):
         else:
             if i < num_inps:
                 # Scalar input, need to store the value in an array of size 1
-                typ = (context.get_data_type(aty)
-                       if not isinstance(aty, types.Boolean)
+                if isinstance(aty, types.Optional):
+                    # Unpack optional type
+                    unpacked_aty = aty.type
+                    arg = context.cast(builder, arg, aty, unpacked_aty)
+                else:
+                    unpacked_aty = aty
+                typ = (context.get_data_type(unpacked_aty)
+                       if not isinstance(unpacked_aty, types.Boolean)
                        else llvmlite.ir.IntType(1))
                 ptr = cgutils.alloca_once(builder, typ)
                 builder.store(arg, ptr)


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
@@ -236,6 +236,8 @@ def copy_args(*args):
                     new_args.append(x.copy())
                 elif isinstance(x, numbers.Number):
                     new_args.append(x)
+                elif x is None:
+                    new_args.append(x)
                 elif isinstance(x, tuple):
                     new_args.append(copy.deepcopy(x))
                 elif isinstance(x, list):
@@ -2130,6 +2132,18 @@ def test_impl(a, b, size):
         self.assertEqual(countParfors(test_impl, cptypes), 2)
         self.check(test_impl, a, b, size)
 
+    def test_prange_optional(self):
+        def test_impl(arr, pred=None):
+            for i in prange(1):
+                if pred is not None:
+                    arr[i] = 0.0
+
+        arr = np.ones(10)
+        self.check(test_impl, arr, None,
+                   check_arg_equality=[np.testing.assert_almost_equal,
+                                       lambda x, y: x == y])
+        self.assertEqual(arr.sum(), 10.0)
+
     def test_untraced_value_tuple(self):
         # This is a test for issue #6478.
         def test_impl():