Repository: numba/numba

Issue:
jitted functions return Python longs, not ints
This is a slight discrepancy with regular Python behaviour. It's annoying for doctests, but it may also have other consequences.

```
>>> @jit
... def f():  return 5
... 
>>> f()
5L
>>> f.py_func()
5
```



Discussion:
After confirming with Siu, I have a solution for this.  The relevant function is in numba.pythonapi.PythonAPI.from_native_value().  On Python 2, we will check for native types that can be represented as a Python int and call PyInt_FromLong() instead of PyLong_FromLongLong().

On all platforms, this should include int8, int16, int32, uint8, and uint16.  If the C long is 64-bit on the platform, then it could also include int64 and uint32.

As a reference, here is the code for `PyInt_FromSsize_t` under Python 2. Our code shouldn't be much more complicated than that :-)

```
PyObject *
PyInt_FromSsize_t(Py_ssize_t ival)
{
    if (ival >= LONG_MIN && ival <= LONG_MAX)
        return PyInt_FromLong((long)ival);
    return _PyLong_FromSsize_t(ival);
}
```


Resolving patch:
diff --git a/numba/pythonapi.py b/numba/pythonapi.py
--- a/numba/pythonapi.py
+++ b/numba/pythonapi.py
@@ -127,16 +127,6 @@ def call(self, callee, args, kws):
         fn = self._get_function(fnty, name="PyObject_Call")
         return self.builder.call(fn, (callee, args, kws))
 
-    def long_from_long(self, ival):
-        fnty = Type.function(self.pyobj, [self.long])
-        fn = self._get_function(fnty, name="PyLong_FromLong")
-        return self.builder.call(fn, [ival])
-
-    def long_from_ssize_t(self, ival):
-        fnty = Type.function(self.pyobj, [self.py_ssize_t])
-        fn = self._get_function(fnty, name="PyLong_FromSsize_t")
-        return self.builder.call(fn, [ival])
-
     def float_from_double(self, fval):
         fnty = Type.function(self.pyobj, [self.double])
         fn = self._get_function(fnty, name="PyFloat_FromDouble")
@@ -162,15 +152,58 @@ def long_as_longlong(self, numobj):
         fn = self._get_function(fnty, name="PyLong_AsLongLong")
         return self.builder.call(fn, [numobj])
 
-    def long_from_ulonglong(self, numobj):
-        fnty = Type.function(self.pyobj, [self.ulonglong])
-        fn = self._get_function(fnty, name="PyLong_FromUnsignedLongLong")
-        return self.builder.call(fn, [numobj])
+    def _long_from_native_int(self, ival, func_name, native_int_type,
+                              signed):
+        fnty = Type.function(self.pyobj, [native_int_type])
+        fn = self._get_function(fnty, name=func_name)
+        resptr = cgutils.alloca_once(self.builder, self.pyobj)
+
+        if PYVERSION < (3, 0):
+            # Under Python 2, we try to return a PyInt object whenever
+            # the given number fits in a C long.
+            pyint_fnty = Type.function(self.pyobj, [self.long])
+            pyint_fn = self._get_function(pyint_fnty, name="PyInt_FromLong")
+            long_max = Constant.int(native_int_type, _helperlib.long_max)
+            if signed:
+                long_min = Constant.int(native_int_type, _helperlib.long_min)
+                use_pyint = self.builder.and_(
+                    self.builder.icmp(lc.ICMP_SGE, ival, long_min),
+                    self.builder.icmp(lc.ICMP_SLE, ival, long_max),
+                    )
+            else:
+                use_pyint = self.builder.icmp(lc.ICMP_ULE, ival, long_max)
 
-    def long_from_longlong(self, numobj):
-        fnty = Type.function(self.pyobj, [self.ulonglong])
-        fn = self._get_function(fnty, name="PyLong_FromLongLong")
-        return self.builder.call(fn, [numobj])
+            with cgutils.ifelse(self.builder, use_pyint) as (then, otherwise):
+                with then:
+                    self.builder.store(self.builder.call(pyint_fn, [ival]), resptr)
+                with otherwise:
+                    self.builder.store(self.builder.call(fn, [ival]), resptr)
+        else:
+            fn = self._get_function(fnty, name=func_name)
+            self.builder.store(self.builder.call(fn, [ival]), resptr)
+
+        return self.builder.load(resptr)
+
+    def long_from_long(self, ival):
+        if PYVERSION < (3, 0):
+            func_name = "PyInt_FromLong"
+        else:
+            func_name = "PyLong_FromLong"
+        fnty = Type.function(self.pyobj, [self.long])
+        fn = self._get_function(fnty, name=func_name)
+        return self.builder.call(fn, [ival])
+
+    def long_from_ssize_t(self, ival):
+        return self._long_from_native_int(ival, "PyLong_FromSsize_t",
+                                          self.py_ssize_t, signed=True)
+
+    def long_from_longlong(self, ival):
+        return self._long_from_native_int(ival, "PyLong_FromLongLong",
+                                          self.longlong, signed=True)
+
+    def long_from_ulonglong(self, ival):
+        return self._long_from_native_int(ival, "PyLong_FromUnsignedLongLong",
+                                          self.ulonglong, signed=False)
 
     def _get_number_operator(self, name):
         fnty = Type.function(self.pyobj, [self.pyobj, self.pyobj])


Test patch:
diff --git a/numba/tests/test_python_int.py b/numba/tests/test_python_int.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_python_int.py
@@ -0,0 +1,56 @@
+from __future__ import print_function
+
+import numba.unittest_support as unittest
+from numba.compiler import compile_isolated, Flags
+from numba import types
+
+
+force_pyobj_flags = Flags()
+force_pyobj_flags.set("force_pyobject")
+
+no_pyobj_flags = Flags()
+
+
+def return_int(a, b):
+    return a + b
+
+
+class TestPythonInt(unittest.TestCase):
+
+    # Issue #474: ints should be returned rather than longs under Python 2,
+    # as much as possible.
+
+    def test_int_return_type(self, flags=force_pyobj_flags,
+                             int_type=types.int64, operands=(3, 4)):
+        pyfunc = return_int
+        cr = compile_isolated(pyfunc, (int_type, int_type), flags=flags)
+        cfunc = cr.entry_point
+        expected = pyfunc(*operands)
+        got = cfunc(*operands)
+        self.assertIs(type(got), type(expected))
+        self.assertEqual(got, expected)
+
+    def test_int_return_type_npm(self):
+        self.test_int_return_type(flags=no_pyobj_flags)
+
+    def test_unsigned_int_return_type(self, flags=force_pyobj_flags):
+        self.test_int_return_type(int_type=types.uint64, flags=flags)
+
+    def test_unsigned_int_return_type_npm(self):
+        self.test_unsigned_int_return_type(flags=no_pyobj_flags)
+
+    def test_long_int_return_type(self, flags=force_pyobj_flags):
+        # Same but returning a 64-bit integer.  The return type should be
+        # `int` on 64-bit builds, `long` on 32-bit ones (or Windows).
+        self.test_int_return_type(flags=flags, operands=(2**33, 2**40))
+
+    def test_long_int_return_type_npm(self):
+        self.test_long_int_return_type(flags=no_pyobj_flags)
+
+    def test_longer_int_return_type(self, flags=force_pyobj_flags):
+        # This won't be supported in nopython mode.
+        self.test_int_return_type(flags=flags, operands=(2**70, 2**75))
+
+
+if __name__ == '__main__':
+    unittest.main()