Repository: numba/numba

Issue:
Invalid use of `make_function` needs catching before lowering
e.g. https://github.com/numba/numba/issues/2992 ends up as `make_function`+`LoweringError`, this particular path ought to be caught earlier and rewritten as a friendly user message as it is almost always down to use of an unsupported construct.



Discussion:

Resolving patch:
diff --git a/numba/compiler.py b/numba/compiler.py
--- a/numba/compiler.py
+++ b/numba/compiler.py
@@ -16,6 +16,7 @@
 from numba.parfor import PreParforPass, ParforPass, Parfor
 from numba.inline_closurecall import InlineClosureCallPass
 from numba.errors import CompilerError
+from numba.ir_utils import raise_on_unsupported_feature
 
 # terminal color markup
 _termcolor = errors.termcolor()
@@ -694,6 +695,9 @@ def stage_compile_interp_mode(self):
                                  lifted=(),
                                  fndesc=None,)
 
+    def stage_ir_legalization(self):
+        raise_on_unsupported_feature(self.func_ir)
+
     def stage_cleanup(self):
         """
         Cleanup intermediate results to release resources.
@@ -759,6 +763,8 @@ def define_nopython_pipeline(self, pm, name='nopython'):
         self.add_pre_typing_stage(pm)
         self.add_typing_stage(pm)
         self.add_optimization_stage(pm)
+        pm.add_stage(self.stage_ir_legalization,
+                     "ensure IR is legal prior to lowering")
         self.add_lowering_stage(pm)
         self.add_cleanup_stage(pm)
 
@@ -770,6 +776,8 @@ def define_objectmode_pipeline(self, pm, name='object'):
         pm.add_stage(self.stage_objectmode_frontend,
                      "object mode frontend")
         pm.add_stage(self.stage_annotate_type, "annotate type")
+        pm.add_stage(self.stage_ir_legalization,
+                     "ensure IR is legal prior to lowering")
         pm.add_stage(self.stage_objectmode_backend, "object mode backend")
         self.add_cleanup_stage(pm)
 
diff --git a/numba/errors.py b/numba/errors.py
--- a/numba/errors.py
+++ b/numba/errors.py
@@ -10,6 +10,7 @@
 import sys
 import warnings
 import numba
+import numpy as np
 from collections import defaultdict
 from numba import six
 from functools import wraps
@@ -498,8 +499,14 @@ class TypingError(NumbaError):
 
 class UntypedAttributeError(TypingError):
     def __init__(self, value, attr, loc=None):
-        msg = "Unknown attribute '{attr}' of type {type}".format(type=value,
-                                                                 attr=attr)
+        module = getattr(value, 'pymod', None)
+        if module is not None and module == np:
+            # unsupported numpy feature.
+            msg = ("Use of unsupported NumPy function 'numpy.%s' "
+                   "or unsupported use of the function.") % attr
+        else:
+            msg = "Unknown attribute '{attr}' of type {type}"
+            msg = msg.format(type=value, attr=attr)
         super(UntypedAttributeError, self).__init__(msg, loc=loc)
 
 
diff --git a/numba/ir_utils.py b/numba/ir_utils.py
--- a/numba/ir_utils.py
+++ b/numba/ir_utils.py
@@ -16,7 +16,7 @@
 from numba.targets.imputils import impl_ret_untracked
 from numba.analysis import (compute_live_map, compute_use_defs,
                             compute_cfg_from_blocks)
-from numba.errors import TypingError
+from numba.errors import TypingError, UnsupportedError
 import copy
 
 _unique_var_count = 0
@@ -1682,3 +1682,43 @@ def is_namedtuple_class(c):
     if not isinstance(fields, tuple):
         return False
     return all(isinstance(f, str) for f in fields)
+
+def raise_on_unsupported_feature(func_ir):
+    """
+    Helper function to walk IR and raise if it finds op codes
+    that are unsupported. Could be extended to cover IR sequences
+    as well as op codes. Intended use is to call it as a pipeline
+    stage just prior to lowering to prevent LoweringErrors for known
+    unsupported features.
+    """
+    for blk in func_ir.blocks.values():
+        for stmt in blk.find_insts(ir.Assign):
+            # This raises on finding `make_function`
+            if isinstance(stmt.value, ir.Expr):
+                if stmt.value.op == 'make_function':
+                    val = stmt.value
+
+                    # See if the construct name can be refined
+                    code = getattr(val, 'code', None)
+                    if code is not None:
+                        # check if this is a closure, the co_name will
+                        # be the captured function name which is not
+                        # useful so be explicit
+                        if getattr(val, 'closure', None) is not None:
+                            use = '<creating a function from a closure>'
+                            expr = ''
+                        else:
+                            use = code.co_name
+                            expr = '(%s) ' % use
+                    else:
+                        use = '<could not ascertain use case>'
+                        expr = ''
+
+                    msg = ("Numba encountered the use of a language "
+                            "feature it does not support in this context: "
+                            "%s (op code: make_function not supported). If "
+                            "the feature is explicitly supported it is "
+                            "likely that the result of the expression %s"
+                            "is being used in an unsupported manner.") % \
+                            (use, expr)
+                    raise UnsupportedError(msg, stmt.value.loc)


Test patch:
diff --git a/numba/tests/test_closure.py b/numba/tests/test_closure.py
--- a/numba/tests/test_closure.py
+++ b/numba/tests/test_closure.py
@@ -8,7 +8,8 @@
 
 import numba.unittest_support as unittest
 from numba import njit, jit, testing, utils
-from numba.errors import NotDefinedError, TypingError, LoweringError
+from numba.errors import (NotDefinedError, TypingError, LoweringError,
+                          UnsupportedError)
 from .support import TestCase, tag
 from numba.six import exec_
 
@@ -425,7 +426,7 @@ def inner(x):
         msg = "Unsupported use of op_LOAD_CLOSURE encountered"
         self.assertIn(msg, str(raises.exception))
 
-        with self.assertRaises(LoweringError) as raises:
+        with self.assertRaises(UnsupportedError) as raises:
             cfunc = jit(nopython=True)(outer11)
             cfunc(var)
         msg = "make_function"
diff --git a/numba/tests/test_dataflow.py b/numba/tests/test_dataflow.py
--- a/numba/tests/test_dataflow.py
+++ b/numba/tests/test_dataflow.py
@@ -197,7 +197,7 @@ def test_for_break_npm(self):
 
     def test_unsupported_op_code(self, flags=force_pyobj_flags):
         pyfunc = unsupported_op_code
-        with self.assertRaises(errors.LoweringError) as raises:
+        with self.assertRaises(errors.UnsupportedError) as raises:
             cr = compile_isolated(pyfunc, (), flags=flags)
         msg="make_function"
         self.assertIn(msg, str(raises.exception))
diff --git a/numba/tests/test_errorhandling.py b/numba/tests/test_errorhandling.py
new file mode 100644
--- /dev/null
+++ b/numba/tests/test_errorhandling.py
@@ -0,0 +1,87 @@
+"""
+Unspecified error handling tests
+"""
+from __future__ import division
+
+from numba import jit, njit
+from numba import unittest_support as unittest
+from numba import errors, utils
+import numpy as np
+
+
+class TestErrorHandlingBeforeLowering(unittest.TestCase):
+
+    expected_msg = ("Numba encountered the use of a language feature it does "
+                    "not support in this context: %s")
+
+    def test_unsupported_make_function_lambda(self):
+        def func(x):
+            f = lambda x: x  # requires `make_function`
+
+        for pipeline in jit, njit:
+            with self.assertRaises(errors.UnsupportedError) as raises:
+                pipeline(func)(1)
+
+            expected = self.expected_msg % "<lambda>"
+            self.assertIn(expected, str(raises.exception))
+
+    def test_unsupported_make_function_listcomp(self):
+        try:
+            @jit
+            def func(x):
+                a = [i for i in x]
+                return undefined_global  # force error
+
+            with self.assertRaises(errors.UnsupportedError) as raises:
+                func([1])
+
+            expected = self.expected_msg % "<listcomp>"
+            self.assertIn(expected, str(raises.exception))
+        except NameError: #py27 cannot handle the undefined global
+            self.assertTrue(utils.PY2)
+
+    def test_unsupported_make_function_dictcomp(self):
+        @jit
+        def func():
+            return {i:0 for i in range(1)}
+
+        with self.assertRaises(errors.UnsupportedError) as raises:
+            func()
+
+        expected = self.expected_msg % "<dictcomp>"
+        self.assertIn(expected, str(raises.exception))
+
+    def test_unsupported_make_function_return_inner_func(self):
+        def func(x):
+            """ return the closure """
+            z = x + 1
+
+            def inner(x):
+                return x + z
+            return inner
+
+        for pipeline in jit, njit:
+            with self.assertRaises(errors.UnsupportedError) as raises:
+                pipeline(func)(1)
+
+            expected = self.expected_msg % \
+                "<creating a function from a closure>"
+            self.assertIn(expected, str(raises.exception))
+
+
+class TestUnsupportedReporting(unittest.TestCase):
+
+    def test_unsupported_numpy_function(self):
+        # np.asarray(list) currently unsupported
+        @njit
+        def func():
+            np.asarray([1,2,3])
+
+        with self.assertRaises(errors.TypingError) as raises:
+            func()
+
+        expected = "Use of unsupported NumPy function 'numpy.asarray'"
+        self.assertIn(expected, str(raises.exception))
+
+if __name__ == '__main__':
+    unittest.main()