Repository: numba/numba

Issue:
Report actual problem in known places of constant inference failure
## Feature request
In cases where there's a constant inference failure with a known target for the constant e.g. argument to a user defined exception, make it such that the error message reported to the user states what the actual problem opposed to just the lack of const-ness.
Unclear error message with ConstantInferenceError
I have following code:
```
@njit
def problem(a,b):
    if a == b:
        raise Exception("Equal numbers: %i %i",a,b)
    return a*b

problem(1,2)
```
Now I know that there is problem with that exception. I can use only constants for Exception constructor. I had very long njit-ed method with multiple exceptions code paths and following error was not helping me to localize the source of problem:
```
---------------------------------------------------------------------------
ConstantInferenceError                    Traceback (most recent call last)
<ipython-input-29-41c708083646> in <module>
      5     return a*b
      6 
----> 7 problem(1,2)

~/.virtualenvs/dl4cv/lib/python3.7/site-packages/numba/dispatcher.py in _compile_for_args(self, *args, **kws)
    358             # this is from trying to infer something as constant when it isn't
    359             # or isn't supported as a constant
--> 360             error_rewrite(e, 'constant_inference')
    361         except Exception as e:
    362             if config.SHOW_HELP:

~/.virtualenvs/dl4cv/lib/python3.7/site-packages/numba/dispatcher.py in error_rewrite(e, issue_type)
    313                 raise e
    314             else:
--> 315                 reraise(type(e), e, None)
    316 
    317         argtypes = []

~/.virtualenvs/dl4cv/lib/python3.7/site-packages/numba/six.py in reraise(tp, value, tb)
    656             value = tp()
    657         if value.__traceback__ is not tb:
--> 658             raise value.with_traceback(tb)
    659         raise value
    660 

ConstantInferenceError: Failed in nopython mode pipeline (step: nopython rewrites)
Cannot make a constant from: Cannot make a constant from: Cannot make a constant from: constant inference not possible for arg(0, name=a)
Numba could not make a constant out of something that it decided should be
a constant. This could well be a current limitation in Numba's internals,
please either raise a bug report along with a minimal reproducer at:
https://github.com/numba/numba/issues/new
```
**Unfortunately there is no information about the line of source code which caused this error**. Would it be possible to encapsulate also line content or just line number into exception raised? Could you navigate me into Numba's source code, maybe I can fix this and create pull request.

**Expected output:**
```
ConstantInferenceError: Failed in nopython mode pipeline (step: nopython rewrites)
Cannot make a constant from: Cannot make a constant from: Cannot make a constant from: constant inference not possible for arg(0, name=a): Source line: 'raise Exception("Equal numbers: %i %i",a,b)'
```

I'm novice, I was playing with Numba just last 2 days and I thank you a lot for this tool. I don't have to write parts of code in plain C. But this lack of helpful error message issue destroyed happy end of my last evening.


Discussion:

Resolving patch:
diff --git a/numba/consts.py b/numba/consts.py
--- a/numba/consts.py
+++ b/numba/consts.py
@@ -5,7 +5,7 @@
 import weakref
 
 from . import ir
-from .errors import ConstantInferenceError
+from .errors import ConstantInferenceError, NumbaError
 
 
 class ConstantInference(object):
@@ -23,7 +23,7 @@ def __init__(self, func_ir):
         self._func_ir = weakref.proxy(func_ir)
         self._cache = {}
 
-    def infer_constant(self, name):
+    def infer_constant(self, name, loc=None):
         """
         Infer a constant value for the given variable *name*.
         If no value can be inferred, numba.errors.ConstantInferenceError
@@ -41,11 +41,19 @@ def infer_constant(self, name):
             return val
         else:
             exc, args = val
-            raise exc(*args)
+            if issubclass(exc, NumbaError):
+                raise exc(*args, loc=loc)
+            else:
+                raise exc(*args)
 
     def _fail(self, val):
+        # The location here is set to None because `val` is the ir.Var name
+        # and not the actual offending use of the var. When this is raised it is
+        # caught in the flow control of `infer_constant` and the class and args
+        # (the message) are captured and then raised again but with the location
+        # set to the expression that caused the constant inference error.
         raise ConstantInferenceError(
-            "constant inference not possible for %s" % (val,))
+            "Constant inference not possible for: %s" % (val,), loc=None)
 
     def _do_infer(self, name):
         if not isinstance(name, str):
@@ -67,15 +75,17 @@ def _do_infer(self, name):
     def _infer_expr(self, expr):
         # Infer an expression: handle supported cases
         if expr.op == 'call':
-            func = self.infer_constant(expr.func.name)
+            func = self.infer_constant(expr.func.name, loc=expr.loc)
             return self._infer_call(func, expr)
         elif expr.op == 'getattr':
-            value = self.infer_constant(expr.value.name)
+            value = self.infer_constant(expr.value.name, loc=expr.loc)
             return self._infer_getattr(value, expr)
         elif expr.op == 'build_list':
-            return [self.infer_constant(i.name) for i in expr.items]
+            return [self.infer_constant(i.name, loc=expr.loc) for i in
+                    expr.items]
         elif expr.op == 'build_tuple':
-            return tuple(self.infer_constant(i.name) for i in expr.items)
+            return tuple(self.infer_constant(i.name, loc=expr.loc) for i in
+                         expr.items)
         self._fail(expr)
 
     def _infer_call(self, func, expr):
@@ -85,7 +95,8 @@ def _infer_call(self, func, expr):
         _slice = func in (slice,)
         _exc = isinstance(func, type) and issubclass(func, BaseException)
         if _slice or _exc:
-            args = [self.infer_constant(a.name) for a in expr.args]
+            args = [self.infer_constant(a.name, loc=expr.loc) for a in
+                    expr.args]
             if _slice:
                 return func(*args)
             elif _exc:
diff --git a/numba/errors.py b/numba/errors.py
--- a/numba/errors.py
+++ b/numba/errors.py
@@ -41,6 +41,7 @@ class ParallelSafetyWarning(RuntimeWarning):
 
 # These are needed in the color formatting of errors setup
 
+
 @six.add_metaclass(abc.ABCMeta)
 class _ColorScheme(object):
 
@@ -109,7 +110,7 @@ def highlight(self, msg):
     # coloring so CI system output is consistently readable without having
     # to read between shell escape characters.
     if os.environ.get('NUMBA_DISABLE_ERROR_MESSAGE_HIGHLIGHTING', None):
-        raise ImportError # just to trigger the exception handler below
+        raise ImportError  # just to trigger the exception handler below
 
 except ImportError:
 
@@ -142,8 +143,7 @@ def termcolor():
 
 else:
 
-    from colorama import init, reinit, deinit, Fore, Back, Style
-    from contextlib import contextmanager
+    from colorama import init, reinit, deinit, Fore, Style
 
     class ColorShell(object):
         _has_initialized = False
@@ -175,10 +175,10 @@ def __exit__(self, *exc_detail):
 
     # No color added, just bold weighting
     themes['no_color'] = {'code': None,
-                         'errmsg': None,
-                         'filename': None,
-                         'indicate': None,
-                         'highlight': None, }
+                          'errmsg': None,
+                          'filename': None,
+                          'indicate': None,
+                          'highlight': None, }
 
     # suitable for terminals with a dark background
     themes['dark_bg'] = {'code': Fore.BLUE,
@@ -203,10 +203,10 @@ def __exit__(self, *exc_detail):
 
     # suitable for use in jupyter notebooks
     themes['jupyter_nb'] = {'code': Fore.BLACK,
-                         'errmsg': Fore.BLACK,
-                         'filename': Fore.GREEN,
-                         'indicate': Fore.CYAN,
-                         'highlight': Fore.RED, }
+                            'errmsg': Fore.BLACK,
+                            'filename': Fore.GREEN,
+                            'indicate': Fore.CYAN,
+                            'highlight': Fore.RED, }
 
     default_theme = themes['no_color']
 
@@ -253,6 +253,15 @@ def termcolor():
             _termcolor_inst = HighlightColorScheme(scheme)
         return _termcolor_inst
 
+feedback_details = """
+Please report the error message and traceback, along with a minimal reproducer
+at: https://github.com/numba/numba/issues/new
+
+If more help is needed please feel free to speak to the Numba core developers
+directly at: https://gitter.im/numba/numba
+
+Thanks in advance for your help in improving Numba!
+"""
 
 unsupported_error_info = """
 Unsupported functionality was found in the code Numba was trying to compile.
@@ -279,9 +288,16 @@ def termcolor():
 constant_inference_info = """
 Numba could not make a constant out of something that it decided should be
 a constant. This could well be a current limitation in Numba's internals,
-please either raise a bug report along with a minimal reproducer at:
-https://github.com/numba/numba/issues/new
-"""
+however please first check that your code is valid for compilation,
+particularly with respect to string interpolation (not supported!) and
+the requirement of compile time constants as arguments to exceptions:
+http://numba.pydata.org/numba-doc/latest/reference/pysupported.html?highlight=exceptions#constructs
+
+If the code is valid and the unsupported functionality is important to you
+please file a feature request at: https://github.com/numba/numba/issues/new
+
+If you think your code should work with Numba. %s
+""" % feedback_details
 
 typing_error_info = """
 This is not usually a problem with Numba itself but instead often caused by
@@ -300,16 +316,6 @@ def termcolor():
 https://github.com/numba/numba/issues/new
 """
 
-feedback_details = """
-Please report the error message and traceback, along with a minimal reproducer
-at: https://github.com/numba/numba/issues/new
-
-If more help is needed please feel free to speak to the Numba core developers
-directly at: https://gitter.im/numba/numba
-
-Thanks in advance for your help in improving Numba!
-"""
-
 reportable_issue_info = """
 -------------------------------------------------------------------------------
 This should not have happened, a problem has occurred in Numba's internals.
@@ -405,7 +411,8 @@ def __init__(self, msg, loc=None, highlighting=True):
         if highlighting:
             highlight = termcolor().errmsg
         else:
-            def highlight(x): return x
+            def highlight(x):
+                return x
         if loc:
             super(NumbaError, self).__init__(
                 highlight("%s\n%s\n" % (msg, loc.strformat())))
@@ -464,6 +471,7 @@ class NotDefinedError(IRError):
     """
     An undefined variable is encountered during interpretation of IR.
     """
+
     def __init__(self, name, loc=None):
         self.name = name
         msg = "Variable '%s' is not defined." % name
@@ -499,6 +507,7 @@ class LoweringError(NumbaError):
     """
     An error occurred during lowering.
     """
+
     def __init__(self, msg, loc=None):
         super(LoweringError, self).__init__(msg, loc=loc)
 
@@ -510,7 +519,6 @@ class UnsupportedParforsError(NumbaError):
     pass
 
 
-
 class ForbiddenConstruct(LoweringError):
     """
     A forbidden Python construct was encountered (e.g. use of locals()).
@@ -542,6 +550,7 @@ class ByteCodeSupportError(NumbaError):
     """
     Failure to extract the bytecode of the user's function.
     """
+
     def __init__(self, msg, loc=None):
         super(ByteCodeSupportError, self).__init__(msg, loc=loc)
 
@@ -557,10 +566,9 @@ class ConstantInferenceError(NumbaError):
     """
     Failure during constant inference.
     """
+
     def __init__(self, value, loc=None):
-        self.value = value
-        msg = "Cannot make a constant from: %s" % value
-        super(ConstantInferenceError, self).__init__(msg, loc=loc)
+        super(ConstantInferenceError, self).__init__(value, loc=loc)
 
 
 class InternalError(NumbaError):
@@ -592,7 +600,6 @@ def _format_msg(fmt, args, kwargs):
     return fmt.format(*args, **kwargs)
 
 
-import os.path
 _numba_path = os.path.dirname(__file__)
 loc_info = {}
 


Test patch:
diff --git a/numba/tests/test_errorhandling.py b/numba/tests/test_errorhandling.py
--- a/numba/tests/test_errorhandling.py
+++ b/numba/tests/test_errorhandling.py
@@ -107,5 +107,24 @@ def f(a):
         self.assertIn(expected, str(raises.exception))
 
 
+class TestConstantInferenceErrorHandling(unittest.TestCase):
+
+    def test_basic_error(self):
+        # issue 3717
+        @njit
+        def problem(a,b):
+            if a == b:
+                raise Exception("Equal numbers: %i %i", a, b)
+            return a * b
+
+        with self.assertRaises(errors.ConstantInferenceError) as raises:
+            problem(1,2)
+
+        msg1 = "Constant inference not possible for: arg(0, name=a)"
+        msg2 = 'raise Exception("Equal numbers: %i %i", a, b)'
+        self.assertIn(msg1, str(raises.exception))
+        self.assertIn(msg2, str(raises.exception))
+
+
 if __name__ == '__main__':
     unittest.main()