Repository: numba/numba

Issue:
Implement two argument round()
We do not yet support the form `round(x, ndigits)` for float32 or float64.  Python (3.4) has two different rounding implementations.  

One tries to ensure that the string representation of the float has the desired number of digits: https://hg.python.org/cpython/file/6dcc96fa3970/Objects/floatobject.c#l865.

The other uses the simpler method of multiplying the value by powers of 10, rounding to an integer, and then dividing by the same factor: https://hg.python.org/cpython/file/6dcc96fa3970/Objects/floatobject.c#l923

We could most easily support the second form in Numba.



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
@@ -1,6 +1,5 @@
 from __future__ import print_function, absolute_import, division
 
-
 import math
 from functools import reduce
 
@@ -1275,11 +1274,12 @@ def round_impl_f32(context, builder, sig, args):
     module = cgutils.get_module(builder)
     fnty = Type.function(Type.float(), [Type.float()])
     if utils.IS_PY3:
-        fn = module.get_or_insert_function(fnty, name="numba.roundf")
+        fn = module.get_or_insert_function(fnty, name="llvm.rint.f32")
+        res = builder.call(fn, args)
+        return builder.fptosi(res, Type.int(32))
     else:
-        fn = module.get_or_insert_function(fnty, name="roundf")
-    assert fn.is_declaration
-    return builder.call(fn, args)
+        fn = module.get_or_insert_function(fnty, name="llvm.round.f32")
+        return builder.call(fn, args)
 
 
 @builtin
@@ -1288,11 +1288,49 @@ def round_impl_f64(context, builder, sig, args):
     module = cgutils.get_module(builder)
     fnty = Type.function(Type.double(), [Type.double()])
     if utils.IS_PY3:
-        fn = module.get_or_insert_function(fnty, name="numba.round")
+        fn = module.get_or_insert_function(fnty, name="llvm.rint.f64")
+        res = builder.call(fn, args)
+        return builder.fptosi(res, Type.int(64))
     else:
-        fn = module.get_or_insert_function(fnty, name="round")
-    assert fn.is_declaration
-    return builder.call(fn, args)
+        fn = module.get_or_insert_function(fnty, name="llvm.round.f64")
+        return builder.call(fn, args)
+
+
+@builtin
+@implement(round, types.Kind(types.Float), types.Kind(types.Integer))
+def round_impl_binary(context, builder, sig, args):
+    fltty = sig.return_type
+
+    def round_ndigits(x, ndigits):
+        if math.isinf(x) or math.isnan(x):
+            return x
+        if ndigits >= 0:
+            if ndigits > 22:
+                # pow1 and pow2 are each safe from overflow, but
+                # pow1*pow2 ~= pow(10.0, ndigits) might overflow.
+                pow1 = 10.0 ** (ndigits - 22)
+                pow2 = 1e22
+            else:
+                pow1 = 10.0 ** ndigits
+                pow2 = 1.0
+            y = (x * pow1) * pow2
+            if math.isinf(y):
+                return x
+        else:
+            pow1 = 10.0 ** (-ndigits)
+            y = x / pow1
+
+        # XXX this does a float -> int -> float roundtrip under Python 3
+        z = round(y)
+        z = math.copysign(z, x)
+        if ndigits >= 0:
+            z = (z / pow2) / pow1
+        else:
+            z *= pow1
+        return z
+
+    return context.compile_internal(builder, round_ndigits, sig, args)
+
 
 #-------------------------------------------------------------------------------
 
diff --git a/numba/targets/externals.py b/numba/targets/externals.py
--- a/numba/targets/externals.py
+++ b/numba/targets/externals.py
@@ -65,10 +65,6 @@ def _do_install(self):
             _add_missing_symbol("__fixunsdfdi", c_helpers["fptoui"])
             _add_missing_symbol("__fixunssfdi", c_helpers["fptouif"])
 
-        # Necessary for Python3
-        ll.add_symbol("numba.round", c_helpers["round_even"])
-        ll.add_symbol("numba.roundf", c_helpers["roundf_even"])
-
         # List available C-math
         for fname in intrinsics.INTR_MATH:
             # Force binding from CPython's C runtime library.
diff --git a/numba/typing/builtins.py b/numba/typing/builtins.py
--- a/numba/typing/builtins.py
+++ b/numba/typing/builtins.py
@@ -720,9 +720,19 @@ def generic(self, args, kws):
 
 class Round(ConcreteTemplate):
     key = round
-    cases = [
-        signature(types.float32, types.float32),
-        signature(types.float64, types.float64),
+    if PYVERSION < (3, 0):
+        cases = [
+            signature(types.float32, types.float32),
+            signature(types.float64, types.float64),
+        ]
+    else:
+        cases = [
+            signature(types.int32, types.float32),
+            signature(types.int64, types.float64),
+        ]
+    cases += [
+        signature(types.float32, types.float32, types.int32),
+        signature(types.float64, types.float64, types.int32),
     ]
 
 


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
@@ -119,9 +119,12 @@ def ord_usecase(x):
 def reduce_usecase(reduce_func, x):
     return functools.reduce(reduce_func, x)
 
-def round_usecase(x):
+def round_usecase1(x):
     return round(x)
 
+def round_usecase2(x, n):
+    return round(x, n)
+
 def sum_usecase(x):
     return sum(x)
 
@@ -630,17 +633,36 @@ def test_reduce_npm(self):
         with self.assertTypingError():
             self.test_reduce(flags=no_pyobj_flags)
 
-    def test_round(self, flags=enable_pyobj_flags):
-        pyfunc = round_usecase
-
-        cr = compile_isolated(pyfunc, (types.float64,), flags=flags)
-        cfunc = cr.entry_point
-        for x in [-0.5, -0.1, 0.0, 0.1, 0.5, 1.5, 5.0]:
-            # XXX round() doesn't return the right type under Python 3
-            self.assertEqual(cfunc(x), pyfunc(x))
-
-    def test_round_npm(self):
-        self.test_round(flags=no_pyobj_flags)
+    def test_round1(self, flags=enable_pyobj_flags):
+        pyfunc = round_usecase1
+
+        for tp in (types.float64, types.float32):
+            cr = compile_isolated(pyfunc, (tp,), flags=flags)
+            cfunc = cr.entry_point
+            for x in [-1.6, -1.5, -1.4, -0.5, -0.1, -0.0,
+                      0.0, 0.1, 0.5, 0.6, 1.4, 1.5, 5.0]:
+                self.assertPreciseEqual(cfunc(x), pyfunc(x))
+
+    def test_round1_npm(self):
+        self.test_round1(flags=no_pyobj_flags)
+
+    def test_round2(self, flags=enable_pyobj_flags):
+        pyfunc = round_usecase2
+
+        for tp in (types.float64, types.float32):
+            prec = 'single' if tp is types.float32 else 'exact'
+            cr = compile_isolated(pyfunc, (tp, types.int32), flags=flags)
+            cfunc = cr.entry_point
+            for x in [0.0, 0.1, 0.125, 0.25, 0.5, 0.75, 1.25,
+                      1.5, 1.75, 2.25, 2.5, 2.75, 12.5, 15.0, 22.5]:
+                for n in (-1, 0, 1, 2):
+                    self.assertPreciseEqual(cfunc(x, n), pyfunc(x, n),
+                                            prec=prec)
+                    self.assertPreciseEqual(cfunc(-x, n), pyfunc(-x, n),
+                                            prec=prec)
+
+    def test_round2_npm(self):
+        self.test_round2(flags=no_pyobj_flags)
 
     def test_sum(self, flags=enable_pyobj_flags):
         pyfunc = sum_usecase