Repository: numba/numba

Issue:
Support the `sum` builtin
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Feature request

<!--

Please include details of the feature you would like to see, why you would
like to see it/the use case

-->
As title, so that e.g. `sum([1,2,3])` works.


Discussion:
Was looking for this for `sum([f(i) for i in ...])` too.

Resolving patch:
diff --git a/numba/cpython/builtins.py b/numba/cpython/builtins.py
--- a/numba/cpython/builtins.py
+++ b/numba/cpython/builtins.py
@@ -8,11 +8,16 @@
 from llvmlite.llvmpy.core import Type, Constant
 import llvmlite.llvmpy.core as lc
 
-from numba.core.imputils import lower_builtin, lower_getattr, lower_getattr_generic, lower_cast, lower_constant, iternext_impl, call_getiter, call_iternext, impl_ret_borrowed, impl_ret_untracked, numba_typeref_ctor
+from numba.core.imputils import (lower_builtin, lower_getattr,
+                                 lower_getattr_generic, lower_cast,
+                                 lower_constant, iternext_impl,
+                                 call_getiter, call_iternext, impl_ret_borrowed,
+                                 impl_ret_untracked, numba_typeref_ctor)
 from numba.core import typing, types, utils, cgutils
 from numba.core.extending import overload, intrinsic
 from numba.core.typeconv import Conversion
 from numba.core.errors import TypingError
+from numba.misc.special import literal_unroll
 
 
 @overload(operator.truth)
@@ -602,6 +607,43 @@ def call_ctor(cls, *args):
 
     return context.compile_internal(builder, call_ctor, sig, args)
 
+
+@overload(sum)
+def ol_sum(iterable, start=0):
+    # Cpython explicitly rejects strings, bytes and bytearrays
+    # https://github.com/python/cpython/blob/3.9/Python/bltinmodule.c#L2310-L2329 # noqa: E501
+    error = None
+    if isinstance(start, types.UnicodeType):
+        error = ('strings', '')
+    elif isinstance(start, types.Bytes):
+        error = ('bytes', 'b')
+    elif isinstance(start, types.ByteArray):
+        error = ('bytearray', 'b')
+
+    if error is not None:
+        msg = "sum() can't sum {} [use {}''.join(seq) instead]".format(*error)
+        raise TypingError(msg)
+
+    # if the container is homogeneous then it's relatively easy to handle.
+    if isinstance(iterable, (types.containers._HomogeneousTuple, types.List,
+                             types.ListType, types.Array, types.RangeType)):
+        iterator = iter
+    elif isinstance(iterable, (types.containers._HeterogeneousTuple)):
+        # if container is heterogeneous then literal unroll and hope for the
+        # best.
+        iterator = literal_unroll
+    else:
+        return None
+
+    def impl(iterable, start=0):
+        acc = start
+        for x in iterator(iterable):
+            # This most likely widens the type, this is expected Numba behaviour
+            acc = acc + x
+        return acc
+    return impl
+
+
 # ------------------------------------------------------------------------------
 # map, filter, reduce
 


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
@@ -2,15 +2,17 @@
 import functools
 import sys
 import operator
+from collections import namedtuple
 
 import numpy as np
 
 import unittest
 from numba.core.compiler import compile_isolated, Flags
-from numba import jit, typeof, njit
+from numba import jit, typeof, njit, typed
 from numba.core import errors, types, utils, config
 from numba.tests.support import TestCase, tag
 
+py38orlater = utils.PYVERSION >= (3, 8)
 
 enable_pyobj_flags = Flags()
 enable_pyobj_flags.set("enable_pyobject")
@@ -137,7 +139,6 @@ def min_usecase3(x):
 def min_usecase4():
     return min(())
 
-
 def oct_usecase(x):
     return oct(x)
 
@@ -207,6 +208,18 @@ def pow_usecase(x, y):
     return pow(x, y)
 
 
+def sum_usecase(x):
+    return sum(x)
+
+
+def sum_kwarg_usecase(x, start=0):
+    ret = sum(x, start)
+    if py38orlater:
+        return sum(x, start=start), ret
+    else:
+        return ret
+
+
 class TestBuiltins(TestCase):
 
     def run_nullary_func(self, pyfunc, flags):
@@ -825,7 +838,7 @@ def test_round2(self, flags=enable_pyobj_flags):
     def test_round2_npm(self):
         self.test_round2(flags=no_pyobj_flags)
 
-    def test_sum(self, flags=enable_pyobj_flags):
+    def test_sum_objmode(self, flags=enable_pyobj_flags):
         pyfunc = sum_usecase
 
         cr = compile_isolated(pyfunc, (types.Dummy('list'),), flags=flags)
@@ -840,9 +853,100 @@ def test_sum(self, flags=enable_pyobj_flags):
         x = [complex(x, x) for x in range(10)]
         self.assertPreciseEqual(cfunc(x), pyfunc(x))
 
-    def test_sum_npm(self):
-        with self.assertTypingError():
-            self.test_sum(flags=no_pyobj_flags)
+    def test_sum(self):
+        # In Python 3.8 "start" can be specified as a kwarg, so test that too
+        sum_default = njit(sum_usecase)
+        sum_kwarg = njit(sum_kwarg_usecase)
+
+        @njit
+        def sum_range(sz, start=0):
+            tmp = range(sz)
+            ret = sum(tmp, start)
+            if py38orlater:
+                return sum(tmp, start=start), ret
+            else:
+                return ret
+
+        ntpl = namedtuple('ntpl', ['a', 'b'])
+
+        # check call with default kwarg, start=0
+        def args():
+            yield [*range(10)]
+            yield [x + x/10.0 for x in range(10)]
+            yield [x * 1j for x in range(10)]
+            yield (1, 2, 3)
+            yield (1, 2, 3j)
+            # uints will likely end up as floats as `start` is signed, so just
+            # test mixed signed ints
+            yield (np.int64(32), np.int32(2), np.int8(3))
+            tl = typed.List(range(5))
+            yield tl
+            yield np.ones(5)
+            yield ntpl(100, 200)
+            yield ntpl(100, 200j)
+
+        for x in args():
+            self.assertPreciseEqual(sum_default(x), sum_default.py_func(x))
+
+        # Check the uint use case, as start is signed, NumPy will end up with
+        # a float result whereas Numba will end up with an int (see integer
+        # typing NBEP).
+        x = (np.uint64(32), np.uint32(2), np.uint8(3))
+        self.assertEqual(sum_default(x), sum_default.py_func(x))
+
+        # check call with changing default kwarg, start
+        def args_kws():
+            yield [*range(10)], 12
+            yield [x + x/10.0 for x in range(10)], 19j
+            yield [x * 1j for x in range(10)], -2
+            yield (1, 2, 3), 9
+            yield (1, 2, 3j), -0
+            # uints will likely end up as floats as `start` is signed, so just
+            # test mixed signed ints
+            yield (np.int64(32), np.int32(2), np.int8(3)), np.uint32(7)
+            tl = typed.List(range(5))
+            yield tl, 100
+            yield np.ones((5, 5)), 10 * np.ones((5,))
+            yield ntpl(100, 200), -50
+            yield ntpl(100, 200j), 9
+
+        for x, start in args_kws():
+            self.assertPreciseEqual(sum_kwarg(x, start=start),
+                                    sum_kwarg.py_func(x, start=start))
+
+        # check call with range()
+        for start in range(-3, 4):
+            for sz in range(-3, 4):
+                self.assertPreciseEqual(sum_range(sz, start=start),
+                                        sum_range.py_func(sz, start=start))
+
+    def test_sum_exceptions(self):
+        sum_default = njit(sum_usecase)
+        sum_kwarg = njit(sum_kwarg_usecase)
+
+        # check start as string/bytes/bytearray is error
+        msg = "sum() can't sum {}"
+
+        with self.assertRaises(errors.TypingError) as raises:
+            sum_kwarg((1, 2, 3), 'a')
+
+        self.assertIn(msg.format('strings'), str(raises.exception))
+
+        with self.assertRaises(errors.TypingError) as raises:
+            sum_kwarg((1, 2, 3), b'123')
+
+        self.assertIn(msg.format('bytes'), str(raises.exception))
+
+        with self.assertRaises(errors.TypingError) as raises:
+            sum_kwarg((1, 2, 3), bytearray(b'123'))
+
+        self.assertIn(msg.format('bytearray'), str(raises.exception))
+
+        # check invalid type has no impl
+        with self.assertRaises(errors.TypingError) as raises:
+            sum_default('abcd')
+
+        self.assertIn('No implementation', str(raises.exception))
 
     def test_truth(self):
         pyfunc = truth_usecase