Repository: numba/numba

Issue:
inline_closure_call._fix_nested_array can fail if array has multiple definitions
If we have this:
```
@nb.njit()
def foo():
    xs = np.empty(10, dtype=np.int32)
    
    if any(xs[i] == xs[i] for i in range(2)):
        return xs

    for i in range(10):
        if i % 4 == 0:
            xs = np.empty(10, dtype=np.int32)

        xs[i] = i

    return xs
```

Then Numba blows up with an obscure error:
```
KeyError: 'Failed at nopython (inline calls to locally defined closures): more than one definition for 'xs''
```

The problem is that x is defined twice, so when `inline_closurecall._fix_nested_array.find_array_def` comes to get the definition for `x` it raises a `KeyError`. Commenting out the `if any(..)` block makes the error go away, probably because then `inline_closurecall` does not fire.

If you replace `arr_def = func_ir.get_definition(arr)` at https://github.com/numba/numba/blob/master/numba/inline_closurecall.py#L794 with the following block then the unmodified code fails to compile with a much better error message (`Untyped global name 'any': cannot determine Numba type of <class 'builtin_function_or_method'>`):

```
try:
    arr_def = func_ir.get_definition(arr)
except KeyError:
    raise GuardException
```

I suspect that the correct fix is probably something else, because I see plenty of uses of `get_definition` within other guarded blocks in the Numba source code, and I suspect the same issue could affect all of them.

(I guess that in some cases this fix may cause code that would otherwise choke Numba to compile successfully, so fixing this bug would do more than just improving error messages.)

(Numba 0.37.0)


Discussion:
Thanks for the report. I think the code above is going to struggle in Numba on a few of accounts:

1. The one noted above, that's clearly a bug that needs fixing.
2. The use of a generator object, which is unsupported.
3. The use of `any` which is unsupported.

Ping @ehsantn @DrTodd13.

MWE:
```python
from numba import njit
import numpy as np

@njit()
def foo():

    # doesn't matter where this is in the code, its just to ensure a
    # `make_function` opcode exists
    [i for i in range(2)]

    xs = np.empty(10)
    for i in range(10):
        xs = np.empty(10)
        xs[i] = i # write is required to trip the bug

    return xs

foo()
```
The use of `any` is really just illustrative: I think the bug can probably be triggered by using any function which triggers the "closure call inlining" to run. I don't really expect numba to be able to compile the example I posted.

Resolving patch:
diff --git a/numba/inline_closurecall.py b/numba/inline_closurecall.py
--- a/numba/inline_closurecall.py
+++ b/numba/inline_closurecall.py
@@ -794,7 +794,7 @@ def find_array_def(arr):
             arr = numba.unsafe.ndarray.empty_inferred(...).
         If it is arr = b[...], find array definition of b recursively.
         """
-        arr_def = func_ir.get_definition(arr)
+        arr_def = get_definition(func_ir, arr)
         _make_debug_print("find_array_def")(arr, arr_def)
         if isinstance(arr_def, ir.Expr):
             if guard(_find_unsafe_empty_inferred, func_ir, arr_def):


Test patch:
diff --git a/numba/tests/test_comprehension.py b/numba/tests/test_comprehension.py
--- a/numba/tests/test_comprehension.py
+++ b/numba/tests/test_comprehension.py
@@ -503,5 +503,22 @@ def f(x):
             return a
         self.check(f, 5, assert_allocate_list=True)
 
+    def test_reuse_of_array_var(self):
+        """ Test issue 3742 """
+        # redefinition of z breaks array comp as there's multiple defn
+        def foo(n):
+            # doesn't matter where this is in the code, it's just to ensure a
+            # `make_function` opcode exists
+            [i for i in range(1)]
+            z = np.empty(n)
+            for i in range(n):
+                z = np.zeros(n)
+                z[i] = i # write is required to trip the bug
+
+            return z
+
+        self.check(foo, 10, assert_allocate_list=True)
+
+
 if __name__ == '__main__':
     unittest.main()