Repository: numba/numba

Issue:
Ufunc calls in the complex domain not properly lowered.
The LLVM code generator in `numba.targets.npyfuncs._dispatch_func_by_name_type()` appears to be improperly lowering function calls that return complex values.  Numba seems to be casting complex types and arguments to tuples (in part via `context.get_argument_type()`), which is not a supported means of specifying complex types in llvmlite.

Example:

``` python
>>> import numpy as np; from numba import *
>>> @njit
... def mypow(x, y): return np.power(x, y)
... 
>>> X = np.ones(10, dtype=np.complex128)
>>> mypow(X, X)
Traceback (most recent call last):
[...]
  File "/home/jriehl/git/jriehl-numba/numba/targets/npyfuncs.py", line 98, in _dispatch_func_by_name_type
    builder.call(fn, call_args)
  File "/home/jriehl/.virtualenvs/numba3/lib/python3.4/site-packages/llvmlite/ir/builder.py", line 455, in call
    cconv=cconv, tail=tail)
  File "/home/jriehl/.virtualenvs/numba3/lib/python3.4/site-packages/llvmlite/ir/instructions.py", line 68, in __init__
    if arg.type != exptype:
AttributeError: 'tuple' object has no attribute 'type'
[...]
numba.lowering.LoweringError: Failed at nopython (nopython mode backend)
Internal error:
AttributeError: 'tuple' object has no attribute 'type'
File "<stdin>", line 2
```

Verified using the current master branch of llvmlite.



Discussion:
There are no complex types in LLVM (nor llvmlite). Operations on complex numbers must be manually decomposed into operations on the real and imaginary parts.

npyfuncs.py needs to be updated to use the datamodel changes properly.  I am taking this one.


Resolving patch:
diff --git a/numba/targets/npyfuncs.py b/numba/targets/npyfuncs.py
--- a/numba/targets/npyfuncs.py
+++ b/numba/targets/npyfuncs.py
@@ -83,18 +83,18 @@ def _dispatch_func_by_name_type(context, builder, sig, args, table, user_name):
         # First, prepare the return value
         complex_class = context.make_complex(ty)
         out = complex_class(context, builder)
-        call_args = [out._getvalue()] + list(args)
+        ptrargs = [cgutils.alloca_once_value(builder, arg)
+                   for arg in args]
+        call_args = [out._getpointer()] + ptrargs
         # get_value_as_argument for struct types like complex allocate stack space
         # and initialize with the value, the return value is the pointer to that
         # allocated space (ie: pointer to a copy of the value in the stack).
         # get_argument_type returns a pointer to the struct type in consonance.
         call_argtys = [ty] + list(sig.args)
-        call_argltys = [context.get_argument_type(ty) for ty in call_argtys]
+        call_argltys = [context.get_value_type(ty).as_pointer()
+                        for ty in call_argtys]
         fnty = lc.Type.function(lc.Type.void(), call_argltys)
         fn = mod.get_or_insert_function(fnty, name=func_name)
-
-        call_args = [context.get_value_as_argument(builder, argty, arg)
-                     for argty, arg in zip(call_argtys, call_args)]
         builder.call(fn, call_args)
         retval = builder.load(call_args[0])
     else:
diff --git a/numba/typing/npydecl.py b/numba/typing/npydecl.py
--- a/numba/typing/npydecl.py
+++ b/numba/typing/npydecl.py
@@ -166,8 +166,8 @@ def generic(self, args, kws):
 # implemented.
 #
 # It also works as a nice TODO list for ufunc support :)
-_unsupported = set([ numpy.frexp, # this one is tricky, as it has 2 returns
-                     numpy.modf,  # this one also has 2 returns
+_unsupported = set([ 'frexp', # this one is tricky, as it has 2 returns
+                     'modf',  # this one also has 2 returns
                  ])
 
 # a list of ufuncs that are in fact aliases of other ufuncs. They need to insert the


Test patch:
diff --git a/numba/tests/test_ufuncs.py b/numba/tests/test_ufuncs.py
--- a/numba/tests/test_ufuncs.py
+++ b/numba/tests/test_ufuncs.py
@@ -658,7 +658,7 @@ def test_bitwise_not_ufunc_npm(self):
         self.test_bitwise_not_ufunc(flags=no_pyobj_flags)
 
 
-    # Note: there is no entry for left_shift and right_shift as this harness 
+    # Note: there is no entry for left_shift and right_shift as this harness
     #       is not valid for them. This is so because left_shift and right
     #       shift implementation in NumPy has undefined behavior (in C-parlance)
     #       when the second argument is a negative (or bigger than the number
@@ -1193,7 +1193,7 @@ def foo(x,y):
         self.assertEqual(foo(1j, 1j), cr.entry_point(1j, 1j))
 
 
-class TestLoopTypes(TestCase):
+class _TestLoopTypes(TestCase):
     """Test code generation for the different loop types defined by ufunc.
 
     This class tests the ufuncs without forcing no-python mode. Subclasses
@@ -1221,8 +1221,9 @@ class TestLoopTypes(TestCase):
     """
 
     _ufuncs = all_ufuncs[:]
+    _ufuncs.remove(np.left_shift) # has its own test class
     _compile_flags = enable_pyobj_flags
-    _skip_types='O'
+    _skip_types = 'OegG'
 
     def _arg_for_type(self, a_letter_type, index=0):
         """return a suitable array argument for testing the letter type"""
@@ -1262,11 +1263,12 @@ def _check_loop(self, fn, ufunc, loop):
         # fail in no python mode. Usually the last loop in ufuncs is an all
         # object fallback
         supported_types = getattr(self, '_supported_types', [])
+        if (supported_types and
+            any(l not in supported_types for l in letter_types)):
+            return
         skip_types = getattr(self, '_skip_types', [])
-        if any(l not in supported_types or l in skip_types
-               for l in letter_types):
+        if any(l in skip_types for l in letter_types):
             return
-
         # if the test case requires some types to be present, skip loops
         # not involving any of those types.
         required_types = getattr(self, '_required_types', [])
@@ -1301,43 +1303,64 @@ def _check_ufunc_with_dtypes(self, fn, ufunc, dtypes):
                 msg = '\n'.join(["ufunc '{0}' arrays differ ({1}):",
                                  "args: {2}", "expected {3}", "got {4}"])
                 msg = msg.format(ufunc.__name__, c_args, prec, py_arg, c_arg)
-                self.assertPreciseEqual(py, c, prec=prec, msg=msg) 
-
-    def _check_ufunc_loops(self, ufunc):
-        fn = _make_ufunc_usecase(ufunc)
-        _failed_loops = []
+                try:
+                    self.assertPreciseEqual(py, c, prec=prec, msg=msg)
+                except AssertionError:
+                    if not self._is_acceptable_error(py, c):
+                        raise
+
+    def _is_acceptable_error(self, py, c):
+        """
+        Result mismatch due to differing error handling.
+        """
+        if np.isnan(c) or np.isnan(py):
+            if 0 in [c, py]:
+                # If one is NaN and the other is zero
+                msg = "mismatch for nan values: expected {0}; got {1}"
+                print(msg.format(py, c))
+                # This is not occurring any more
+
+        elif np.abs(c) == 0 and np.abs(py) == np.abs(c):
+            # If both are zeros and they are differ by sign
+            msg = "mismatch sign for 0: expected {0}; got {1}"
+            print(msg.format(py, c))
+            return True
+
+        elif isinstance(c, complex) and isinstance(py, complex):
+            # Recurse into real and imag of a complex
+            return (self._is_acceptable_error(c.real, py.real)
+                    and self._is_acceptable_error(c.imag, py.imag))
+
+        # Match?
+        return py == c
+
+    @classmethod
+    def _check_ufunc_loops(cls, ufunc):
         for loop in ufunc.types:
-            try:
-                self._check_loop(fn, ufunc, loop)
-            except AssertionError as e:
-                import traceback
-                traceback.print_exc()
-                _failed_loops.append('{2} {0}:{1}'.format(loop, str(e),
-                                                          ufunc.__name__))
-
-        return _failed_loops
-
-    def test_ufunc_loops(self):
-        failed_ufuncs = []
-        failed_loops_count = 0
-        for ufunc in self._ufuncs:
-            failed_loops = self._check_ufunc_loops(ufunc)
-            if failed_loops:
-                failed_loops_count += len(failed_loops)
-                msg = 'ufunc {0} failed in loops:\n\t{1}\n\t'.format(
-                    ufunc.__name__,
-                    '\n\t'.join(failed_loops))
-                failed_ufuncs.append(msg)
-
-        if failed_ufuncs:
-            msg = 'Failed {0} ufuncs, {1} loops:\n{2}'.format(
-                len(failed_ufuncs), failed_loops_count,
-                '\n'.join(failed_ufuncs))
-
-            self.fail(msg=msg)
-
-
-class TestLoopTypesIntNoPython(TestLoopTypes):
+            cls._inject_test(ufunc, loop)
+
+    @classmethod
+    def _inject_test(cls, ufunc, loop):
+        def test_template(self):
+            fn = _make_ufunc_usecase(ufunc)
+            self._check_loop(fn, ufunc, loop)
+        setattr(cls, "test_{0}_{1}".format(ufunc.__name__,
+                                           loop.replace('->', '_')),
+                test_template)
+
+    @classmethod
+    def autogenerate(cls):
+        for ufunc in cls._ufuncs:
+            cls._check_ufunc_loops(ufunc)
+
+
+class TestLoopTypes(_TestLoopTypes):
+    pass
+
+TestLoopTypes.autogenerate()
+
+
+class TestLoopTypesIntNoPython(_TestLoopTypes):
     _compile_flags = no_pyobj_flags
     _ufuncs = supported_ufuncs[:]
     # reciprocal needs a special test due to issue #757
@@ -1345,32 +1368,34 @@ class TestLoopTypesIntNoPython(TestLoopTypes):
     _ufuncs.remove(np.left_shift) # has its own test class
     _ufuncs.remove(np.right_shift) # has its own test class
     _required_types = '?bBhHiIlLqQ'
-    _skip_types = 'fdFDmMO'
+    _skip_types = 'fdFDmMO' + _TestLoopTypes._skip_types
 
+TestLoopTypesIntNoPython.autogenerate()
 
-class TestLoopTypesIntReciprocalNoPython(TestLoopTypes):
+class TestLoopTypesIntReciprocalNoPython(_TestLoopTypes):
     _compile_flags = no_pyobj_flags
     _ufuncs = [np.reciprocal] # issue #757
     _required_types = 'bBhHiIlLqQ'
-    _skip_types = 'fdFDmMO'
+    _skip_types = 'fdFDmMO' + _TestLoopTypes._skip_types
 
     def _arg_for_type(self, a_letter_type, index=0):
-        res = super(self.__class__, self)._arg_for_type(self.a_letter_type,
+        res = super(self.__class__, self)._arg_for_type(a_letter_type,
                                                         index=index)
         # avoid 0 as argument, as it triggers undefined behavior that my differ
         # in results from numba to the compiler used to compile NumPy
         res[res == 0] = 42
         return res
 
+TestLoopTypesIntReciprocalNoPython.autogenerate()
 
-class TestLoopTypesIntLeftShiftNoPython(TestLoopTypes):
+class TestLoopTypesIntLeftShiftNoPython(_TestLoopTypes):
     _compile_flags = no_pyobj_flags
     _ufuncs = [np.left_shift]
     _required_types = 'bBhHiIlLqQ'
-    _skip_types = 'fdFDmMO'
+    _skip_types = 'fdFDmMO' + _TestLoopTypes._skip_types
 
     def _arg_for_type(self, a_letter_type, index=0):
-        res = super(self.__class__, self)._arg_for_type(self.a_letter_type,
+        res = super(self.__class__, self)._arg_for_type(a_letter_type,
                                                         index=index)
         # Shifting by a negative amount (argument with index 1) is undefined
         # behavior in C. It is also undefined behavior in numba. In the same
@@ -1380,18 +1405,19 @@ def _arg_for_type(self, a_letter_type, index=0):
         # that 0 <= shift_amount < bitcount(shifted_integer)
         if index == 1:
             bit_count = res.dtype.itemsize * 8
-            res = np.clip(res, 0, bit_count-1) 
+            res = np.clip(res, 0, bit_count-1)
         return res
 
+TestLoopTypesIntLeftShiftNoPython.autogenerate()
 
-class TestLoopTypesIntRightShiftNoPython(TestLoopTypes):
+class TestLoopTypesIntRightShiftNoPython(_TestLoopTypes):
     _compile_flags = no_pyobj_flags
     _ufuncs = [np.right_shift]
     _required_types = 'bBhHiIlLqQ'
-    _skip_types = 'fdFDmMO'
+    _skip_types = 'fdFDmMO' + _TestLoopTypes._skip_types
 
     def _arg_for_type(self, a_letter_type, index=0):
-        res = super(self.__class__, self)._arg_for_type(self.a_letter_type,
+        res = super(self.__class__, self)._arg_for_type(a_letter_type,
                                                         index=index)
         # Shifting by a negative amount (argument with index 1) is undefined
         # behavior in C. It is also undefined behavior in numba. In the same
@@ -1407,7 +1433,7 @@ def _arg_for_type(self, a_letter_type, index=0):
         # shifted is negative (in C). In numba, right shift for signed integers
         # is "arithmetic" while for unsigned integers is "logical".
         # This test compares against the NumPy implementation, that relies
-        # on "implementation defined behavior", so the test could be a false 
+        # on "implementation defined behavior", so the test could be a false
         # failure if the compiler used to compile NumPy doesn't follow the same
         # policy.
         # Hint: do not rely on right shifting negative numbers in NumPy.
@@ -1415,28 +1441,31 @@ def _arg_for_type(self, a_letter_type, index=0):
             res = np.abs(res)
         return res
 
+TestLoopTypesIntRightShiftNoPython.autogenerate()
 
-class TestLoopTypesFloatNoPython(TestLoopTypes):
+class TestLoopTypesFloatNoPython(_TestLoopTypes):
     _compile_flags = no_pyobj_flags
     _ufuncs = supported_ufuncs[:]
     if iswindows:
         _ufuncs.remove(np.signbit) # TODO: fix issue #758
     _required_types = 'fd'
-    _skip_types = 'FDmMO'
+    _skip_types = 'FDmMO' + _TestLoopTypes._skip_types
 
+TestLoopTypesFloatNoPython.autogenerate()
 
-class TestLoopTypesComplexNoPython(TestLoopTypes):
+class TestLoopTypesComplexNoPython(_TestLoopTypes):
     _compile_flags = no_pyobj_flags
     _ufuncs = supported_ufuncs[:]
 
     # Test complex types
     # Every loop containing a complex argument must be tested
     _required_types = 'FD'
-    _skip_types = 'mMO'
+    _skip_types = 'mMO' + _TestLoopTypes._skip_types
 
+TestLoopTypesComplexNoPython.autogenerate()
 
 @skip_on_numpy_16
-class TestLoopTypesDatetimeNoPython(TestLoopTypes):
+class TestLoopTypesDatetimeNoPython(_TestLoopTypes):
     _compile_flags = no_pyobj_flags
     _ufuncs = supported_ufuncs[:]
 
@@ -1530,6 +1559,7 @@ def test_comparisons(self):
                       np.greater, np.greater_equal]:
             self._check_comparison(ufunc)
 
+TestLoopTypesDatetimeNoPython.autogenerate()
 
 class TestUFuncBadArgsNoPython(TestCase):
     _compile_flags = no_pyobj_flags