Repository: numba/numba

Issue:
`max` fails typing on tuples
For example:

```
>>> import numba as nb                                                                                                          
>>> @nb.njit                                                                                                                 
... def foo(thing):
...     return max(thing)                                                                                                       
...                                                                                                                             
>>> foo((1,2))                                                                                                                  
Traceback (most recent call last):                                                                                              
  File "<stdin>", line 1, in <module>
  File "<PATH>numba/dispatcher.py", line 304, in _compile_for_args
    raise e
  File "<PATH>numba/dispatcher.py", line 281, in _compile_for_args
    return self.compile(tuple(argtypes))
  File "<PATH>numba/dispatcher.py", line 527, in compile
    cres = self._compiler.compile(args, return_type)
  File "<PATH>numba/dispatcher.py", line 76, in compile
    flags=flags, locals=self.locals)
  File "<PATH>numba/compiler.py", line 695, in compile_extra
    return pipeline.compile_extra(func)
  File "<PATH>numba/compiler.py", line 369, in compile_extra
    return self.compile_bytecode(bc, func_attr=self.func_attr)
  File "<PATH>numba/compiler.py", line 378, in compile_bytecode
    return self._compile_bytecode()
  File "<PATH>numba/compiler.py", line 661, in _compile_bytecode
    res = pm.run(self.status)
  File "<PATH>numba/compiler.py", line 257, in run
    raise patched_exception
  File "<PATH>numba/compiler.py", line 249, in run
    stage()
  File "<PATH>numba/compiler.py", line 465, in stage_nopython_frontend
    self.locals)
  File "<PATH>numba/compiler.py", line 789, in type_inference_stage
    infer.propagate()
  File "<PATH>numba/typeinfer.py", line 717, in propagate
    raise errors[0]
  File "<PATH>numba/typeinfer.py", line 127, in propagate
    constraint(typeinfer)
  File "<PATH>numba/typeinfer.py", line 372, in __call__
    self.resolve(typeinfer, typevars, fnty)
  File "<PATH>numba/typeinfer.py", line 394, in resolve
    raise TypingError(msg, loc=self.loc)
numba.errors.TypingError: Failed at nopython (nopython frontend)
Invalid usage of Function(<built-in function max>) with parameters ((int64 x 2))
 * parameterized
File "<stdin>", line 3
[1] During: resolving callee type: Function(<built-in function max>)
[2] During: typing of call at <stdin> (3)
```



Discussion:

Resolving patch:
diff --git a/numba/targets/builtins.py b/numba/targets/builtins.py
--- a/numba/targets/builtins.py
+++ b/numba/targets/builtins.py
@@ -97,54 +97,49 @@ def setitem_cpointer(context, builder, sig, args):
 
 #-------------------------------------------------------------------------------
 
-@lower_builtin(max, types.VarArg(types.Any))
-def max_impl(context, builder, sig, args):
-    argtys = sig.args
-    for a in argtys:
-        if a not in types.number_domain:
-            raise AssertionError("only implemented for numeric types")
-
-    def domax(a, b):
-        at, av = a
-        bt, bv = b
-        ty = context.typing_context.unify_types(at, bt)
+def do_minmax(context, builder, argtys, args, cmpop):
+    assert len(argtys) == len(args), (argtys, args)
+    assert len(args) > 0
+
+    def binary_minmax(accumulator, value):
+        # This is careful to reproduce Python's algorithm, e.g.
+        # max(1.5, nan, 2.5) should return 2.5 (not nan or 1.5)
+        accty, acc = accumulator
+        vty, v = value
+        ty = context.typing_context.unify_types(accty, vty)
         assert ty is not None
-        cav = context.cast(builder, av, at, ty)
-        cbv = context.cast(builder, bv, bt, ty)
+        acc = context.cast(builder, acc, accty, ty)
+        v = context.cast(builder, v, vty, ty)
         cmpsig = typing.signature(types.boolean, ty, ty)
-        ge = context.get_function(">=", cmpsig)
-        pred = ge(builder, (cav, cbv))
-        res = builder.select(pred, cav, cbv)
+        ge = context.get_function(cmpop, cmpsig)
+        pred = ge(builder, (v, acc))
+        res = builder.select(pred, v, acc)
         return ty, res
 
     typvals = zip(argtys, args)
-    resty, resval = reduce(domax, typvals)
-    return impl_ret_borrowed(context, builder, sig.return_type, resval)
+    resty, resval = reduce(binary_minmax, typvals)
+    return resval
 
 
-@lower_builtin(min, types.VarArg(types.Any))
-def min_impl(context, builder, sig, args):
-    argtys = sig.args
-    for a in argtys:
-        if a not in types.number_domain:
-            raise AssertionError("only implemented for numeric types")
-
-    def domax(a, b):
-        at, av = a
-        bt, bv = b
-        ty = context.typing_context.unify_types(at, bt)
-        assert ty is not None
-        cav = context.cast(builder, av, at, ty)
-        cbv = context.cast(builder, bv, bt, ty)
-        cmpsig = typing.signature(types.boolean, ty, ty)
-        le = context.get_function("<=", cmpsig)
-        pred = le(builder, (cav, cbv))
-        res = builder.select(pred, cav, cbv)
-        return ty, res
+@lower_builtin(max, types.BaseTuple)
+def max_iterable(context, builder, sig, args):
+    argtys = list(sig.args[0])
+    args = cgutils.unpack_tuple(builder, args[0])
+    return do_minmax(context, builder, argtys, args, '>')
 
-    typvals = zip(argtys, args)
-    resty, resval = reduce(domax, typvals)
-    return impl_ret_borrowed(context, builder, sig.return_type, resval)
+@lower_builtin(max, types.VarArg(types.Any))
+def max_vararg(context, builder, sig, args):
+    return do_minmax(context, builder, sig.args, args, '>')
+
+@lower_builtin(min, types.BaseTuple)
+def min_iterable(context, builder, sig, args):
+    argtys = list(sig.args[0])
+    args = cgutils.unpack_tuple(builder, args[0])
+    return do_minmax(context, builder, argtys, args, '<')
+
+@lower_builtin(min, types.VarArg(types.Any))
+def min_vararg(context, builder, sig, args):
+    return do_minmax(context, builder, sig.args, args, '<')
 
 
 def _round_intrinsic(tp):
diff --git a/numba/typing/builtins.py b/numba/typing/builtins.py
--- a/numba/typing/builtins.py
+++ b/numba/typing/builtins.py
@@ -643,40 +643,47 @@ def register_number_classes(register_global):
 #------------------------------------------------------------------------------
 
 
-@infer_global(max)
-class Max(AbstractTemplate):
+class MinMaxBase(AbstractTemplate):
+
+    def _unify_minmax(self, tys):
+        for ty in tys:
+            if not isinstance(ty, types.Number):
+                return
+        return self.context.unify_types(*tys)
 
     def generic(self, args, kws):
+        """
+        Resolve a min() or max() call.
+        """
         assert not kws
 
-        # max(a, b, ...)
-        if len(args) < 2:
+        if not args:
             return
-        for a in args:
-            if a not in types.number_domain:
+        if len(args) == 1:
+            # max(arg) only supported if arg is an iterable
+            if isinstance(args[0], types.BaseTuple):
+                tys = list(args[0])
+                if not tys:
+                    raise TypeError("%s() argument is an empty tuple"
+                                    % (self.key.__name__,))
+            else:
                 return
-
-        retty = self.context.unify_types(*args)
+        else:
+            # max(*args)
+            tys = args
+        retty = self._unify_minmax(tys)
         if retty is not None:
             return signature(retty, *args)
 
 
-@infer_global(min)
-class Min(AbstractTemplate):
-
-    def generic(self, args, kws):
-        assert not kws
+@infer_global(max)
+class Max(MinMaxBase):
+    pass
 
-        # min(a, b, ...)
-        if len(args) < 2:
-            return
-        for a in args:
-            if a not in types.number_domain:
-                return
 
-        retty = self.context.unify_types(*args)
-        if retty is not None:
-            return signature(retty, *args)
+@infer_global(min)
+class Min(MinMaxBase):
+    pass
 
 
 @infer_global(round)


Test patch:
diff --git a/numba/tests/test_builtins.py b/numba/tests/test_builtins.py
--- a/numba/tests/test_builtins.py
+++ b/numba/tests/test_builtins.py
@@ -102,6 +102,7 @@ def long_usecase(x, base):
 def map_usecase(x, map_func):
     return map(map_func, x)
 
+
 def max_usecase1(x, y):
     return max(x, y)
 
@@ -111,6 +112,10 @@ def max_usecase2(x, y):
 def max_usecase3(x):
     return max(x)
 
+def max_usecase4():
+    return max(())
+
+
 def min_usecase1(x, y):
     return min(x, y)
 
@@ -120,6 +125,10 @@ def min_usecase2(x, y):
 def min_usecase3(x):
     return min(x)
 
+def min_usecase4():
+    return min(())
+
+
 def oct_usecase(x):
     return oct(x)
 
@@ -514,8 +523,11 @@ def test_map_npm(self):
         with self.assertTypingError():
             self.test_map(flags=no_pyobj_flags)
 
-    def test_max_1(self, flags=enable_pyobj_flags):
-        pyfunc = max_usecase1
+    #
+    # min() and max()
+    #
+
+    def check_minmax_1(self, pyfunc, flags):
         cr = compile_isolated(pyfunc, (types.int32, types.int32), flags=flags)
         cfunc = cr.entry_point
 
@@ -524,26 +536,27 @@ def test_max_1(self, flags=enable_pyobj_flags):
         for x, y in itertools.product(x_operands, y_operands):
             self.assertPreciseEqual(cfunc(x, y), pyfunc(x, y))
 
-    def test_max_2(self, flags=enable_pyobj_flags):
-        pyfunc = max_usecase2
-        cr = compile_isolated(pyfunc, (types.int32, types.int32), flags=flags)
-        cfunc = cr.entry_point
+    def test_max_1(self, flags=enable_pyobj_flags):
+        """
+        max(*args)
+        """
+        self.check_minmax_1(max_usecase1, flags)
 
-        x_operands = [-1, 0, 1]
-        y_operands = [-1, 0, 1]
-        for x, y in itertools.product(x_operands, y_operands):
-            self.assertPreciseEqual(cfunc(x, y), pyfunc(x, y))
+    def test_min_1(self, flags=enable_pyobj_flags):
+        """
+        min(*args)
+        """
+        self.check_minmax_1(min_usecase1, flags)
 
     @tag('important')
     def test_max_npm_1(self):
         self.test_max_1(flags=no_pyobj_flags)
 
-    def test_max_npm_2(self):
-        with self.assertTypingError():
-            self.test_max_2(flags=no_pyobj_flags)
+    @tag('important')
+    def test_min_npm_1(self):
+        self.test_min_1(flags=no_pyobj_flags)
 
-    def test_min_1(self, flags=enable_pyobj_flags):
-        pyfunc = min_usecase1
+    def check_minmax_2(self, pyfunc, flags):
         cr = compile_isolated(pyfunc, (types.int32, types.int32), flags=flags)
         cfunc = cr.entry_point
 
@@ -552,24 +565,58 @@ def test_min_1(self, flags=enable_pyobj_flags):
         for x, y in itertools.product(x_operands, y_operands):
             self.assertPreciseEqual(cfunc(x, y), pyfunc(x, y))
 
-    def test_min_2(self, flags=enable_pyobj_flags):
-        pyfunc = min_usecase2
-        cr = compile_isolated(pyfunc, (types.int32, types.int32), flags=flags)
-        cfunc = cr.entry_point
+    def test_max_2(self, flags=enable_pyobj_flags):
+        """
+        max(list)
+        """
+        self.check_minmax_2(max_usecase2, flags)
 
-        x_operands = [-1, 0, 1]
-        y_operands = [-1, 0, 1]
-        for x, y in itertools.product(x_operands, y_operands):
-            self.assertPreciseEqual(cfunc(x, y), pyfunc(x, y))
+    def test_min_2(self, flags=enable_pyobj_flags):
+        """
+        min(list)
+        """
+        self.check_minmax_2(min_usecase2, flags)
 
-    @tag('important')
-    def test_min_npm_1(self):
-        self.test_min_1(flags=no_pyobj_flags)
+    def test_max_npm_2(self):
+        with self.assertTypingError():
+            self.test_max_2(flags=no_pyobj_flags)
 
     def test_min_npm_2(self):
         with self.assertTypingError():
             self.test_min_2(flags=no_pyobj_flags)
 
+    def check_minmax_3(self, pyfunc, flags):
+        def check(argty):
+            cr = compile_isolated(pyfunc, (argty,), flags=flags)
+            cfunc = cr.entry_point
+            # Check that the algorithm matches Python's with a non-total order
+            tup = (1.5, float('nan'), 2.5)
+            for val in [tup, tup[::-1]]:
+                self.assertPreciseEqual(cfunc(val), pyfunc(val))
+
+        check(types.UniTuple(types.float64, 3))
+        check(types.Tuple((types.float32, types.float64, types.float32)))
+
+    def test_max_3(self, flags=enable_pyobj_flags):
+        """
+        max(tuple)
+        """
+        self.check_minmax_3(max_usecase3, flags)
+
+    def test_min_3(self, flags=enable_pyobj_flags):
+        """
+        min(tuple)
+        """
+        self.check_minmax_3(min_usecase3, flags)
+
+    @tag('important')
+    def test_max_npm_3(self):
+        self.test_max_3(flags=no_pyobj_flags)
+
+    @tag('important')
+    def test_min_npm_3(self):
+        self.test_min_3(flags=no_pyobj_flags)
+
     def check_min_max_invalid_types(self, pyfunc, flags=enable_pyobj_flags):
         cr = compile_isolated(pyfunc, (types.int32, types.Dummy('list')),
                               flags=flags)
@@ -623,6 +670,21 @@ def test_min_unary_non_iterable_npm(self):
         with self.assertTypingError():
             self.check_min_max_unary_non_iterable(min_usecase3)
 
+    # Test that max(()) and min(()) fail
+
+    def check_min_max_empty_tuple(self, pyfunc, func_name):
+        with self.assertTypingError() as raises:
+            compile_isolated(pyfunc, (), flags=no_pyobj_flags)
+        self.assertIn("%s() argument is an empty tuple" % func_name,
+                      str(raises.exception))
+
+    def test_max_empty_tuple(self):
+        self.check_min_max_empty_tuple(max_usecase4, "max")
+
+    def test_min_empty_tuple(self):
+        self.check_min_max_empty_tuple(min_usecase4, "min")
+
+
     def test_oct(self, flags=enable_pyobj_flags):
         pyfunc = oct_usecase