Repository: numba/numba

Issue:
Exec functions with assertions fail
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I am using the latest ~released~ master 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).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

Functions instantiated by `exec` that have assertion statements fail to compile, probably due to the need for source code lines in #3325.

```python
f_text = "def f(a):\n  assert a != 1"
loc = {}
exec(f_text, {}, loc)
ff = loc['f']
numba.njit(ff)(2)
```
```
~/pse-hpc/python/numba/numba/lowering.py in lower_inst(self, inst)
    398
    399         elif isinstance(inst, ir.StaticRaise):
--> 400             self.lower_static_raise(inst)
    401
    402         else:

~/pse-hpc/python/numba/numba/lowering.py in lower_static_raise(self, inst)
    437             self.return_exception(None, loc=self.loc)
    438         else:
--> 439             self.return_exception(inst.exc_class, inst.exc_args, loc=self.loc)
    440
    441     def lower_assign(self, ty, inst):

~/pse-hpc/python/numba/numba/lowering.py in return_exception(self, exc_class, exc_args, loc)
    159     def return_exception(self, exc_class, exc_args=None, loc=None):
    160         self.call_conv.return_user_exc(self.builder, exc_class, exc_args,
--> 161                                        loc=loc)
    162
    163     def emit_environment_object(self):

~/pse-hpc/python/numba/numba/targets/callconv.py in return_user_exc(self, builder, exc, exc_args, loc)
    352         # Build excinfo struct
    353         if loc is not None:
--> 354             locinfo = (loc._raw_function_name(), loc.filename, loc.line)
    355             if None in locinfo:
    356                 locinfo = None

~/pse-hpc/python/numba/numba/ir.py in _raw_function_name(self)
     60     def _raw_function_name(self):
     61         defn = self._find_definition()
---> 62         return self._defmatcher.match(defn.strip()).groups()[0]
     63
     64     def _get_lines(self):

LoweringError: Failed in nopython mode pipeline (step: nopython mode backend)
'NoneType' object has no attribute 'strip'

File "<string>", line 2:
<source missing, REPL in use?>

[1] During: lowering "raise <class 'AssertionError'>" at <string> (2)
```


Discussion:

Resolving patch:
diff --git a/numba/ir.py b/numba/ir.py
--- a/numba/ir.py
+++ b/numba/ir.py
@@ -59,7 +59,11 @@ def _find_definition(self):
 
     def _raw_function_name(self):
         defn = self._find_definition()
-        return self._defmatcher.match(defn.strip()).groups()[0]
+        if defn:
+            return self._defmatcher.match(defn.strip()).groups()[0]
+        else:
+            # Probably exec(<string>) or REPL.
+            return None
 
     def _get_lines(self):
         if self.lines is None:
@@ -130,7 +134,7 @@ def count_spaces(string):
 
         # if in the REPL source may not be available
         if not ret:
-            ret = "<source missing, REPL in use?>"
+            ret = "<source missing, REPL/exec in use?>"
 
         err = _termcolor.filename('\nFile "%s", line %d:')+'\n%s'
         tmp = err % (self._get_path(), self.line, _termcolor.code(''.join(ret)))
diff --git a/numba/lowering.py b/numba/lowering.py
--- a/numba/lowering.py
+++ b/numba/lowering.py
@@ -158,7 +158,8 @@ def pre_block(self, block):
 
     def return_exception(self, exc_class, exc_args=None, loc=None):
         self.call_conv.return_user_exc(self.builder, exc_class, exc_args,
-                                       loc=loc)
+                                       loc=loc,
+                                       func_name=self.func_ir.func_id.func_name)
 
     def emit_environment_object(self):
         """Emit a pointer to hold the Environment object.
diff --git a/numba/targets/callconv.py b/numba/targets/callconv.py
--- a/numba/targets/callconv.py
+++ b/numba/targets/callconv.py
@@ -186,7 +186,8 @@ def return_value(self, builder, retval):
         builder.store(retval, retptr)
         self._return_errcode_raw(builder, RETCODE_OK)
 
-    def return_user_exc(self, builder, exc, exc_args=None, loc=None):
+    def return_user_exc(self, builder, exc, exc_args=None, loc=None,
+                        func_name=None):
         if exc is not None and not issubclass(exc, BaseException):
             raise TypeError("exc should be None or exception class, got %r"
                             % (exc,))
@@ -197,8 +198,12 @@ def return_user_exc(self, builder, exc, exc_args=None, loc=None):
         pyapi = self.context.get_python_api(builder)
         # Build excinfo struct
         if loc is not None:
-            f1 = loc._raw_function_name()
-            locinfo = (f1, loc.filename, loc.line)
+            fname = loc._raw_function_name()
+            if fname is None:
+                # could be exec(<string>) or REPL, try func_name
+                fname = func_name
+
+            locinfo = (fname, loc.filename, loc.line)
             if None in locinfo:
                 locinfo = None
         else:
@@ -340,7 +345,8 @@ def return_value(self, builder, retval):
         builder.store(retval, retptr)
         self._return_errcode_raw(builder, RETCODE_OK)
 
-    def return_user_exc(self, builder, exc, exc_args=None, loc=None):
+    def return_user_exc(self, builder, exc, exc_args=None, loc=None,
+                        func_name=None):
         if exc is not None and not issubclass(exc, BaseException):
             raise TypeError("exc should be None or exception class, got %r"
                             % (exc,))
@@ -351,7 +357,12 @@ def return_user_exc(self, builder, exc, exc_args=None, loc=None):
         pyapi = self.context.get_python_api(builder)
         # Build excinfo struct
         if loc is not None:
-            locinfo = (loc._raw_function_name(), loc.filename, loc.line)
+            fname = loc._raw_function_name()
+            if fname is None:
+                # could be exec(<string>) or REPL, try func_name
+                fname = func_name
+
+            locinfo = (fname, loc.filename, loc.line)
             if None in locinfo:
                 locinfo = None
         else:


Test patch:
diff --git a/numba/tests/test_exceptions.py b/numba/tests/test_exceptions.py
--- a/numba/tests/test_exceptions.py
+++ b/numba/tests/test_exceptions.py
@@ -218,6 +218,24 @@ def test_assert_statement_objmode(self):
     def test_assert_statement_nopython(self):
         self.check_assert_statement(flags=no_pyobj_flags)
 
+    def check_raise_from_exec_string(self, flags):
+        # issue #3428
+        f_text = "def f(a):\n  assert a != 1"
+        loc = {}
+        exec(f_text, {}, loc)
+        pyfunc = loc['f']
+        cres = compile_isolated(pyfunc, (types.int32,), flags=flags)
+        cfunc = cres.entry_point
+        cfunc(2)
+        self.check_against_python(flags, pyfunc, cfunc, AssertionError, 1)
+
+    def test_assert_from_exec_string_objmode(self):
+        self.check_raise_from_exec_string(flags=force_pyobj_flags)
+
+    def test_assert_from_exec_string_nopython(self):
+        self.check_raise_from_exec_string(flags=no_pyobj_flags)
+
+
 
 if __name__ == '__main__':
     unittest.main()