Repository: numba/numba

Issue:
min/max behaves incorrectly on 0-size array
Consider the following code:
```python
In [324]: def test_0d_min(X):
     ...:     return np.min(X)
     ...:
     ...:

In [325]: test_0d_min(np.array([]))  # correct behavior
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
... (omitted)
ValueError: zero-size array to reduction operation minimum which has no identity

In [326]: nb.jit(test_0d_min)(np.array([]))  # incorrect
Out[326]: 0.0

In [328]: nb.jit(test_0d_min, parallel=True)(np.array([]))  # incorrect
Out[328]: 1.7976931348623157e+308
```

I guess that parallelization for reduction functions is done by calculating each block seperately and then combining them together, so an identity element is used as the initial result of each block. `np.finfo(np.float64)` shows that `1.7976931348623157e+308` is exactly the largest `np.float64` instance, which seems suitable as an identity element of `np.min`.

But actually reduction function (aka folding) and identity element are both defined over binary functions, while min function is obviously an n-ary function (a function that accepts down to 0, up to n arguments) mathematically.

As for non-parallel version of jitted function, I have no idea why it computes to 0.


Discussion:
Just searched wiki and found that [it says min/max also has an identity element](https://en.wikipedia.org/wiki/Identity_element). Well, it looks just reasonable, but I still think consistent behavior through numpy and numba is beneficial

Resolving patch:
diff --git a/numba/parfor.py b/numba/parfor.py
--- a/numba/parfor.py
+++ b/numba/parfor.py
@@ -16,7 +16,7 @@
 import types as pytypes  # avoid confusion with numba.types
 import sys, math
 from functools import reduce
-from collections import defaultdict
+from collections import defaultdict, namedtuple
 from contextlib import contextmanager
 
 import numba
@@ -26,6 +26,7 @@
 from numba.typing.templates import infer_global, AbstractTemplate
 from numba import stencilparfor
 from numba.stencilparfor import StencilPass
+from numba.extending import register_jitable
 
 
 from numba.ir_utils import (
@@ -111,6 +112,7 @@ def min_parallel_impl(return_type, arg):
     if arg.ndim == 1:
         def min_1(in_arr):
             numba.parfor.init_prange()
+            min_checker(len(in_arr))
             val = numba.targets.builtins.get_type_max_value(in_arr.dtype)
             for i in numba.parfor.internal_prange(len(in_arr)):
                 val = min(val, in_arr[i])
@@ -118,6 +120,7 @@ def min_1(in_arr):
     else:
         def min_1(in_arr):
             numba.parfor.init_prange()
+            min_checker(len(in_arr))
             val = numba.targets.builtins.get_type_max_value(in_arr.dtype)
             for i in numba.pndindex(in_arr.shape):
                 val = min(val, in_arr[i])
@@ -128,6 +131,7 @@ def max_parallel_impl(return_type, arg):
     if arg.ndim == 1:
         def max_1(in_arr):
             numba.parfor.init_prange()
+            max_checker(len(in_arr))
             val = numba.targets.builtins.get_type_min_value(in_arr.dtype)
             for i in numba.parfor.internal_prange(len(in_arr)):
                 val = max(val, in_arr[i])
@@ -135,6 +139,7 @@ def max_1(in_arr):
     else:
         def max_1(in_arr):
             numba.parfor.init_prange()
+            max_checker(len(in_arr))
             val = numba.targets.builtins.get_type_min_value(in_arr.dtype)
             for i in numba.pndindex(in_arr.shape):
                 val = max(val, in_arr[i])
@@ -143,6 +148,7 @@ def max_1(in_arr):
 
 def argmin_parallel_impl(in_arr):
     numba.parfor.init_prange()
+    argmin_checker(len(in_arr))
     A = in_arr.ravel()
     init_val = numba.targets.builtins.get_type_max_value(A.dtype)
     ival = numba.typing.builtins.IndexValue(0, init_val)
@@ -153,6 +159,7 @@ def argmin_parallel_impl(in_arr):
 
 def argmax_parallel_impl(in_arr):
     numba.parfor.init_prange()
+    argmax_checker(len(in_arr))
     A = in_arr.ravel()
     init_val = numba.targets.builtins.get_type_min_value(A.dtype)
     ival = numba.typing.builtins.IndexValue(0, init_val)
@@ -378,6 +385,8 @@ def linspace_3(start, stop, num):
     ('argmax', 'numpy'): lambda r,a: argmax_parallel_impl,
     ('min', 'numpy'): min_parallel_impl,
     ('max', 'numpy'): max_parallel_impl,
+    ('amin', 'numpy'): min_parallel_impl,
+    ('amax', 'numpy'): max_parallel_impl,
     ('sum', 'numpy'): sum_parallel_impl,
     ('prod', 'numpy'): prod_parallel_impl,
     ('mean', 'numpy'): mean_parallel_impl,
@@ -388,6 +397,40 @@ def linspace_3(start, stop, num):
     ('linspace', 'numpy'): linspace_parallel_impl,
 }
 
+@register_jitable
+def max_checker(arr_size):
+    if arr_size == 0:
+        raise ValueError(("zero-size array to reduction operation "
+                            "maximum which has no identity"))
+
+@register_jitable
+def min_checker(arr_size):
+    if arr_size == 0:
+        raise ValueError(("zero-size array to reduction operation "
+                            "minimum which has no identity"))
+
+@register_jitable
+def argmin_checker(arr_size):
+    if arr_size == 0:
+        raise ValueError("attempt to get argmin of an empty sequence")
+
+@register_jitable
+def argmax_checker(arr_size):
+    if arr_size == 0:
+        raise ValueError("attempt to get argmax of an empty sequence")
+
+checker_impl = namedtuple('checker_impl', ['name', 'func'])
+
+replace_functions_checkers_map = {
+    ('argmin', 'numpy') : checker_impl('argmin_checker', argmin_checker),
+    ('argmax', 'numpy') : checker_impl('argmax_checker', argmax_checker),
+    ('min', 'numpy') : checker_impl('min_checker', min_checker),
+    ('max', 'numpy') : checker_impl('max_checker', max_checker),
+    ('amin', 'numpy') : checker_impl('min_checker', min_checker),
+    ('amax', 'numpy') : checker_impl('max_checker', max_checker),
+}
+
+
 class LoopNest(object):
 
     '''The LoopNest class holds information of a single loop including
@@ -586,6 +629,12 @@ def replace_func():
                             g['numba'] = numba
                             g['np'] = numpy
                             g['math'] = math
+                            # if the function being inlined has a function
+                            # checking the inputs, find it and add it to globals
+                            check = replace_functions_checkers_map.get(callname,
+                                                                       None)
+                            if check is not None:
+                                g[check.name] = check.func
                             # inline the parallel implementation
                             inline_closure_call(self.func_ir, g,
                                             block, i, new_func, self.typingctx, typs,
diff --git a/numba/targets/arraymath.py b/numba/targets/arraymath.py
--- a/numba/targets/arraymath.py
+++ b/numba/targets/arraymath.py
@@ -369,6 +369,9 @@ def array_min(context, builder, sig, args):
         nat = ty('NaT')
 
         def array_min_impl(arry):
+            if arry.size == 0:
+                raise ValueError(("zero-size array to reduction operation "
+                                  "minimum which has no identity"))
             min_value = nat
             it = np.nditer(arry)
             for view in it:
@@ -385,6 +388,9 @@ def array_min_impl(arry):
 
     else:
         def array_min_impl(arry):
+            if arry.size == 0:
+                raise ValueError(("zero-size array to reduction operation "
+                                  "minimum which has no identity"))
             it = np.nditer(arry)
             for view in it:
                 min_value = view.item()
@@ -403,6 +409,9 @@ def array_min_impl(arry):
 @lower_builtin("array.max", types.Array)
 def array_max(context, builder, sig, args):
     def array_max_impl(arry):
+        if arry.size == 0:
+            raise ValueError(("zero-size array to reduction operation "
+                                "maximum which has no identity"))
         it = np.nditer(arry)
         for view in it:
             max_value = view.item()
@@ -431,6 +440,8 @@ def array_argmin(context, builder, sig, args):
         nat = ty('NaT')
 
         def array_argmin_impl(arry):
+            if arry.size == 0:
+                raise ValueError("attempt to get argmin of an empty sequence")
             min_value = nat
             min_idx = 0
             it = arry.flat
@@ -452,6 +463,8 @@ def array_argmin_impl(arry):
 
     else:
         def array_argmin_impl(arry):
+            if arry.size == 0:
+                raise ValueError("attempt to get argmin of an empty sequence")
             for v in arry.flat:
                 min_value = v
                 min_idx = 0
@@ -472,6 +485,8 @@ def array_argmin_impl(arry):
 @lower_builtin("array.argmax", types.Array)
 def array_argmax(context, builder, sig, args):
     def array_argmax_impl(arry):
+        if arry.size == 0:
+            raise ValueError("attempt to get argmax of an empty sequence")
         for v in arry.flat:
             max_value = v
             max_idx = 0


Test patch:
diff --git a/numba/tests/test_array_reductions.py b/numba/tests/test_array_reductions.py
--- a/numba/tests/test_array_reductions.py
+++ b/numba/tests/test_array_reductions.py
@@ -673,5 +673,47 @@ def new_test_function(self, redFunc=red_func,
 TestArrayReductions.install_generated_tests()
 
 
+class TestArrayReductionsExceptions(MemoryLeakMixin, TestCase):
+
+    # int64, size 0
+    zero_size = np.arange(0)
+
+    def check_exception(self, pyfunc, msg):
+        cfunc = jit(nopython=True)(pyfunc)
+        # make sure NumPy raises consistently/no behaviour change
+        with self.assertRaises(BaseException):
+            pyfunc(self.zero_size)
+        # check numba impl raises expected
+        with self.assertRaises(ValueError) as e:
+            cfunc(self.zero_size)
+        self.assertIn(msg, str(e.exception))
+
+    @classmethod
+    def install(cls):
+
+        fn_to_msg = dict()
+        empty_seq = "attempt to get {0} of an empty sequence"
+        op_no_ident = ("zero-size array to reduction operation "
+                       "{0}")
+        for x in [array_argmax, array_argmax_global, array_argmin,
+                  array_argmin_global]:
+            fn_to_msg[x] = empty_seq
+        for x in [array_max, array_max, array_min, array_min]:
+            fn_to_msg[x] = op_no_ident
+
+        name_template = "test_zero_size_array_{0}"
+        for fn, msg in fn_to_msg.items():
+            test_name = name_template.format(fn.__name__)
+
+            lmsg = msg.format(fn.__name__)
+            lmsg = lmsg.replace('array_','').replace('_global','')
+            def test_fn(self, func=fn, message=lmsg):
+                self.check_exception(func, message)
+
+            setattr(cls, test_name, test_fn)
+
+TestArrayReductionsExceptions.install()
+
+
 if __name__ == '__main__':
     unittest.main()
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
@@ -523,16 +523,16 @@ def test_impl(n):
 
     @skip_unsupported
     @tag('important')
-    def test_fuse_argmin(self):
-        def test_impl(n):
-            A = np.ones(n)
-            C = A.argmin()
-            B = A.sum()
-            return B+C
-
-        self.check(test_impl, 256)
-        self.assertTrue(countParfors(test_impl, (types.int64, )) == 1)
-        self.assertTrue(countArrays(test_impl, (types.intp,)) == 0)
+    def test_fuse_argmin_argmax_max_min(self):
+        for op in [np.argmin, np.argmax, np.min, np.max]:
+            def test_impl(n):
+                A = np.ones(n)
+                C = op(A)
+                B = A.sum()
+                return B + C
+            self.check(test_impl, 256)
+            self.assertTrue(countParfors(test_impl, (types.int64, )) == 1)
+            self.assertTrue(countArrays(test_impl, (types.intp,)) == 0)
 
     @skip_unsupported
     @tag('important')
@@ -965,6 +965,15 @@ def test_impl2(A):
         self.check(test_impl2, B)
         self.check(test_impl2, C)
 
+        # checks that 0d array input raises
+        msg = ("zero-size array to reduction operation "
+               "minimum which has no identity")
+        for impl in (test_impl1, test_impl2):
+            pcfunc = self.compile_parallel(impl, (types.int64[:],))
+            with self.assertRaises(ValueError) as e:
+                pcfunc.entry_point(np.array([], dtype=np.int64))
+            self.assertIn(msg, str(e.exception))
+
     @skip_unsupported
     def test_max(self):
         def test_impl1(A):
@@ -984,6 +993,15 @@ def test_impl2(A):
         self.check(test_impl2, B)
         self.check(test_impl2, C)
 
+        # checks that 0d array input raises
+        msg = ("zero-size array to reduction operation "
+               "maximum which has no identity")
+        for impl in (test_impl1, test_impl2):
+            pcfunc = self.compile_parallel(impl, (types.int64[:],))
+            with self.assertRaises(ValueError) as e:
+                pcfunc.entry_point(np.array([], dtype=np.int64))
+            self.assertIn(msg, str(e.exception))
+
     @skip_unsupported
     def test_argmin(self):
         def test_impl1(A):
@@ -1003,6 +1021,14 @@ def test_impl2(A):
         self.check(test_impl2, B)
         self.check(test_impl2, C)
 
+        # checks that 0d array input raises
+        msg = 'attempt to get argmin of an empty sequence'
+        for impl in (test_impl1, test_impl2):
+            pcfunc = self.compile_parallel(impl, (types.int64[:],))
+            with self.assertRaises(ValueError) as e:
+                pcfunc.entry_point(np.array([], dtype=np.int64))
+            self.assertIn(msg, str(e.exception))
+
     @skip_unsupported
     def test_argmax(self):
         def test_impl1(A):
@@ -1022,6 +1048,13 @@ def test_impl2(A):
         self.check(test_impl2, B)
         self.check(test_impl2, C)
 
+        # checks that 0d array input raises
+        msg = 'attempt to get argmax of an empty sequence'
+        for impl in (test_impl1, test_impl2):
+            pcfunc = self.compile_parallel(impl, (types.int64[:],))
+            with self.assertRaises(ValueError) as e:
+                pcfunc.entry_point(np.array([], dtype=np.int64))
+            self.assertIn(msg, str(e.exception))
 
     @skip_unsupported
     def test_parfor_array_access1(self):