Repository: numba/numba

Issue:
ord builtin not supported
## Reporting a bug

- [X] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [X] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

I'm expecting the simple builtin function `ord` to be supported in nopython mode. Instead I get the error `Untyped global name 'ord': cannot determine Numba type of <class 'builtin_function_or_method'>`.

For a running example see https://repl.it/@TomGoBravo/numba-ord


Discussion:

Resolving patch:
diff --git a/numba/cpython/unicode.py b/numba/cpython/unicode.py
--- a/numba/cpython/unicode.py
+++ b/numba/cpython/unicode.py
@@ -66,6 +66,9 @@
 
 _py38_or_later = utils.PYVERSION >= (3, 8)
 
+# https://github.com/python/cpython/blob/1d4b6ba19466aba0eb91c4ba01ba509acf18c723/Objects/unicodeobject.c#L84-L85    # noqa: E501
+_MAX_UNICODE = 0x10ffff
+
 # DATA MODEL
 
 
@@ -2277,6 +2280,55 @@ def _unicode_swapcase(data, length, res, maxchars):
 def unicode_swapcase(data):
     return case_operation(_ascii_swapcase, _unicode_swapcase)
 
+
+# https://github.com/python/cpython/blob/1d4b6ba19466aba0eb91c4ba01ba509acf18c723/Python/bltinmodule.c#L1781-L1824    # noqa: E501
+@overload(ord)
+def ol_ord(c):
+    if isinstance(c, types.UnicodeType):
+        def impl(c):
+            lc = len(c)
+            if lc != 1:
+                # CPython does TypeError
+                raise TypeError("ord() expected a character")
+            return _get_code_point(c, 0)
+        return impl
+
+
+# https://github.com/python/cpython/blob/1d4b6ba19466aba0eb91c4ba01ba509acf18c723/Objects/unicodeobject.c#L2005-L2028    # noqa: E501
+# This looks a bit different to the cpython implementation but, with the
+# exception of a latin1 fast path is logically the same. It finds the "kind" of
+# the codepoint `ch`, creates a length 1 string of that kind and then injects
+# the code point into the zero position of that string. Cpython does similar but
+# branches for each kind (this is encapsulated in Numba's _set_code_point).
+@register_jitable
+def _unicode_char(ch):
+    assert ch <= _MAX_UNICODE
+    kind = _codepoint_to_kind(ch)
+    ret = _empty_string(kind, 1, kind == PY_UNICODE_1BYTE_KIND)
+    _set_code_point(ret, 0, ch)
+    return ret
+
+
+_out_of_range_msg = "chr() arg not in range(0x%hx)" % _MAX_UNICODE
+
+
+# https://github.com/python/cpython/blob/1d4b6ba19466aba0eb91c4ba01ba509acf18c723/Objects/unicodeobject.c#L3045-L3055    # noqa: E501
+@register_jitable
+def _PyUnicode_FromOrdinal(ordinal):
+    if (ordinal < 0 or ordinal > _MAX_UNICODE):
+        raise ValueError(_out_of_range_msg)
+
+    return _unicode_char(_Py_UCS4(ordinal))
+
+
+# https://github.com/python/cpython/blob/1d4b6ba19466aba0eb91c4ba01ba509acf18c723/Python/bltinmodule.c#L715-L720    # noqa: E501
+@overload(chr)
+def ol_chr(i):
+    if isinstance(i, types.Integer):
+        def impl(i):
+            return _PyUnicode_FromOrdinal(i)
+        return impl
+
 # ------------------------------------------------------------------------------
 # iteration
 # ------------------------------------------------------------------------------


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
@@ -50,12 +50,6 @@ def any_usecase(x, y):
 def bool_usecase(x):
     return bool(x)
 
-def chr_usecase(x):
-    return chr(x)
-
-def cmp_usecase(x, y):
-    return cmp(x, y)
-
 def complex_usecase(x, y):
     return complex(x, y)
 
@@ -144,9 +138,6 @@ def min_usecase4():
 def oct_usecase(x):
     return oct(x)
 
-def ord_usecase(x):
-    return ord(x)
-
 def reduce_usecase(reduce_func, x):
     return functools.reduce(reduce_func, x)
 
@@ -320,18 +311,6 @@ def test_bool_nonnumber_npm(self):
         with self.assertTypingError():
             self.test_bool_nonnumber(flags=no_pyobj_flags)
 
-    def test_chr(self, flags=enable_pyobj_flags):
-        pyfunc = chr_usecase
-
-        cr = compile_isolated(pyfunc, (types.int32,), flags=flags)
-        cfunc = cr.entry_point
-        for x in range(256):
-            self.assertPreciseEqual(cfunc(x), pyfunc(x))
-
-    def test_chr_npm(self):
-        with self.assertTypingError():
-            self.test_chr(flags=no_pyobj_flags)
-
     def test_complex(self, flags=enable_pyobj_flags):
         pyfunc = complex_usecase
 
@@ -747,18 +726,6 @@ def test_oct_npm(self):
         with self.assertTypingError():
             self.test_oct(flags=no_pyobj_flags)
 
-    def test_ord(self, flags=enable_pyobj_flags):
-        pyfunc = ord_usecase
-
-        cr = compile_isolated(pyfunc, (types.string,), flags=flags)
-        cfunc = cr.entry_point
-        for x in ['a', u'\u2020']:
-            self.assertPreciseEqual(cfunc(x), pyfunc(x))
-
-    def test_ord_npm(self):
-        with self.assertTypingError():
-            self.test_ord(flags=no_pyobj_flags)
-
     def test_reduce(self, flags=enable_pyobj_flags):
         pyfunc = reduce_usecase
         cr = compile_isolated(pyfunc, (types.Dummy('function_ptr'),
diff --git a/numba/tests/test_unicode.py b/numba/tests/test_unicode.py
--- a/numba/tests/test_unicode.py
+++ b/numba/tests/test_unicode.py
@@ -7,6 +7,7 @@
 import unittest
 from numba.tests.support import (TestCase, no_pyobj_flags, MemoryLeakMixin)
 from numba.core.errors import TypingError
+from numba.cpython.unicode import _MAX_UNICODE
 
 
 _py37_or_later = utils.PYVERSION >= (3, 7)
@@ -338,6 +339,14 @@ def lower_usecase(x):
     return x.lower()
 
 
+def ord_usecase(x):
+    return ord(x)
+
+
+def chr_usecase(x):
+    return chr(x)
+
+
 class BaseTest(MemoryLeakMixin, TestCase):
     def setUp(self):
         super(BaseTest, self).setUp()
@@ -2389,5 +2398,61 @@ def test_unicode_literal_stopiteration_iter(self):
                 f()
 
 
+class TestUnicodeAuxillary(BaseTest):
+
+    def test_ord(self):
+        pyfunc = ord_usecase
+        cfunc = njit(pyfunc)
+        for ex in UNICODE_EXAMPLES:
+            for a in ex:
+                self.assertPreciseEqual(pyfunc(a), cfunc(a))
+
+    def test_ord_invalid(self):
+        self.disable_leak_check()
+
+        pyfunc = ord_usecase
+        cfunc = njit(pyfunc)
+
+        # wrong number of chars
+        for func in (pyfunc, cfunc):
+            for ch in ('', 'abc'):
+                with self.assertRaises(TypeError) as raises:
+                    func(ch)
+                self.assertIn('ord() expected a character',
+                              str(raises.exception))
+
+        # wrong type
+        with self.assertRaises(TypingError) as raises:
+            cfunc(1.23)
+        self.assertIn('Invalid use of Function', str(raises.exception))
+
+    def test_chr(self):
+        pyfunc = chr_usecase
+        cfunc = njit(pyfunc)
+        for ex in UNICODE_EXAMPLES:
+            for x in ex:
+                a = ord(x)
+                self.assertPreciseEqual(pyfunc(a), cfunc(a))
+        # test upper/lower bounds
+        for a in (0x0, _MAX_UNICODE):
+            self.assertPreciseEqual(pyfunc(a), cfunc(a))
+
+    def test_chr_invalid(self):
+        pyfunc = chr_usecase
+        cfunc = njit(pyfunc)
+
+        # value negative/>_MAX_UNICODE
+        for func in (pyfunc, cfunc):
+            for v in (-2, _MAX_UNICODE + 1):
+                with self.assertRaises(ValueError) as raises:
+                    func(v)
+                self.assertIn("chr() arg not in range", str(raises.exception))
+
+        # wrong type
+        with self.assertRaises(TypingError) as raises:
+            cfunc('abc')
+        self.assertIn('Invalid use of Function', str(raises.exception))
+
+
 if __name__ == '__main__':
     unittest.main()