Repository: numba/numba

Issue:
Empty list passed into lifted loop causes compilation to fail

Sample code from mailing list:

``` python
from numpy import *
import numpy as np
from numpy.random import randint
from numba import jit, vectorize


@vectorize()
def rint(i):
    return randint(i)

@jit
def neighbourhood(n):
    I = eye(n,dtype=byte)
    return vstack([-I, I])

@jit(nopython=True)
def cluster(lattice, prob):
    nn = neighbourhood(lattice.ndim)
    dims = array(lattice.shape)
    n=0
    S=[]
    start = rint(dims)
    state = not lattice[start]
    new = start[newaxis,:]
    while True:
        lattice[new.T] = state
        S.extend(new)
        if not S:
            break
        n+=1
        current = S.pop()
        neighbours = ((current + nn) % dims)
        candidates = logical_xor(state, lattice[neighbours.T])
        flips = logical_and(candidates,
                            np.random.random(len(candidates)) < prob)
        new=neighbours[flips]
    return n*(2*state-1)

d=2**9
A = zeros((d,d), dtype=bool)

cluster(A, 0.9)
print cluster.inspect_types()
```

Produces this error:
```
Traceback (most recent call last):
  File "test.py", line 42, in <module>
    cluster(A, 0.9)
ValueError: cannot compute fingerprint of empty list
```

In Numba 0.22.1, this fell back to object mode, and no error was produced.

Empty list passed into lifted loop causes compilation to fail

Sample code from mailing list:

``` python
from numpy import *
import numpy as np
from numpy.random import randint
from numba import jit, vectorize


@vectorize()
def rint(i):
    return randint(i)

@jit
def neighbourhood(n):
    I = eye(n,dtype=byte)
    return vstack([-I, I])

@jit(nopython=True)
def cluster(lattice, prob):
    nn = neighbourhood(lattice.ndim)
    dims = array(lattice.shape)
    n=0
    S=[]
    start = rint(dims)
    state = not lattice[start]
    new = start[newaxis,:]
    while True:
        lattice[new.T] = state
        S.extend(new)
        if not S:
            break
        n+=1
        current = S.pop()
        neighbours = ((current + nn) % dims)
        candidates = logical_xor(state, lattice[neighbours.T])
        flips = logical_and(candidates,
                            np.random.random(len(candidates)) < prob)
        new=neighbours[flips]
    return n*(2*state-1)

d=2**9
A = zeros((d,d), dtype=bool)

cluster(A, 0.9)
print cluster.inspect_types()
```

Produces this error:
```
Traceback (most recent call last):
  File "test.py", line 42, in <module>
    cluster(A, 0.9)
ValueError: cannot compute fingerprint of empty list
```

In Numba 0.22.1, this fell back to object mode, and no error was produced.



Discussion:

Resolving patch:
diff --git a/numba/dispatcher.py b/numba/dispatcher.py
--- a/numba/dispatcher.py
+++ b/numba/dispatcher.py
@@ -142,7 +142,7 @@ class _DispatcherBase(_dispatcher.Dispatcher):
 
     __numba__ = "py_func"
 
-    def __init__(self, arg_count, py_func, pysig):
+    def __init__(self, arg_count, py_func, pysig, can_fallback):
         self._tm = default_type_manager
 
         # A mapping of signatures to compile results
@@ -166,6 +166,7 @@ def __init__(self, arg_count, py_func, pysig):
         _dispatcher.Dispatcher.__init__(self, self._tm.get_pointer(),
                                         arg_count, self._fold_args,
                                         argnames, defargs,
+                                        can_fallback,
                                         has_stararg)
 
         self.doc = py_func.__doc__
@@ -424,8 +425,8 @@ def __init__(self, py_func, locals={}, targetoptions={}, impl_kind='direct'):
 
         pysig = utils.pysignature(py_func)
         arg_count = len(pysig.parameters)
-
-        _DispatcherBase.__init__(self, arg_count, py_func, pysig)
+        can_fallback = not targetoptions.get('nopython', False)
+        _DispatcherBase.__init__(self, arg_count, py_func, pysig, can_fallback)
 
         functools.update_wrapper(self, py_func)
 
@@ -578,7 +579,8 @@ def __init__(self, func_ir, typingctx, targetctx, flags, locals):
 
         _DispatcherBase.__init__(self, self.func_ir.arg_count,
                                  self.func_ir.func_id.func,
-                                 self.func_ir.func_id.pysig)
+                                 self.func_ir.func_id.pysig,
+                                 can_fallback=True)
 
     def get_source_location(self):
         """Return the starting line number of the loop.


Test patch:
diff --git a/numba/tests/test_dispatcher.py b/numba/tests/test_dispatcher.py
--- a/numba/tests/test_dispatcher.py
+++ b/numba/tests/test_dispatcher.py
@@ -16,6 +16,7 @@
 
 from numba import unittest_support as unittest
 from numba import utils, vectorize, jit, generated_jit, types, appdirs
+from numba import _dispatcher
 from numba.errors import NumbaWarning
 from .support import TestCase, tag, temp_directory, import_dynamic
 
@@ -244,6 +245,43 @@ def bar():
         m = "No matching definition for argument type(s) array(float64, 1d, C)"
         self.assertEqual(str(raises.exception), m)
 
+    def test_fingerprint_failure(self):
+        """
+        Failure in computing the fingerprint cannot affect a nopython=False
+        function.  On the other hand, with nopython=True, a ValueError should
+        be raised to report the failure with fingerprint.
+        """
+        @jit
+        def foo(x):
+            return x
+
+        # Empty list will trigger failure in compile_fingerprint
+        errmsg = 'cannot compute fingerprint of empty list'
+        with self.assertRaises(ValueError) as raises:
+            _dispatcher.compute_fingerprint([])
+        self.assertIn(errmsg, str(raises.exception))
+        # It should work in fallback
+        self.assertEqual(foo([]), [])
+        # But, not in nopython=True
+        strict_foo = jit(nopython=True)(foo.py_func)
+        with self.assertRaises(ValueError) as raises:
+            strict_foo([])
+        self.assertIn(errmsg, str(raises.exception))
+
+        # Test in loop lifting context
+        @jit
+        def bar():
+            object()  # force looplifting
+            x = []
+            for i in range(10):
+                x = foo(x)
+            return x
+
+        self.assertEqual(bar(), [])
+        # Make sure it was looplifted
+        [cr] = bar.overloads.values()
+        self.assertEqual(len(cr.lifted), 1)
+
 
 class TestSignatureHandling(BaseTest):
     """