Repository: numba/numba

Issue:
Numba is raising a `NotImplementedError` instead of the `NumbaNotImplementedError` equivalent
<!--

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).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

While trying the new error model with [RBC](https://github.com/xnd-project/rbc/), I found a `NotImplementedError` left on `numba/core/base.py` which I suspect it should be a `errors.NumbaNotImplementedError`. This causes the compilation process to crash because the exception is not captured anymore.


Discussion:
Closing since this is not an actual issue.
Reopening this issue since there's a case not treated with the new error handling model
Is that related to #7508 or is it a separate case?
Sort of related to #7508. There is a case that is not handled in `numba.core.base.py::BaseContext.get_function`.
https://github.com/numba/numba/blob/af2cd6f67617005524cec5b4e8a51633bc1900fc/numba/core/base.py#L537-L570

Suppose it is the first call to this function and there are some registries that needs to be updated.
1. get_function(fn, sig, _firstcall=True)
    - `_wrap_impl(...)` raises `NumbaNotImplementedError` from `overloads.find`
    - Code will enter the `if isinstance(fn, types.Type)` statement
        - Calls `get_function` with `type(fn)` as first argument
2. get_function(type(fn), sig, _firstcall=True)
    - `_wrap_impl(...)` raises `NumbaNotImplementedError` from `overloads.find`
    - `isinstance` check will fail because `fn` is already a `Type`
    - code will enter the `if _firstcall:` statement
        - Calls `self.refresh()` and later `get_function(..., _firstcall=False)`
3. get_function(type(fn), sig, _firstcall=False)
    - `_wrap_impl(...)` raises `NumbaNotImplementedError` from `overloads.find`
    - `isinstance` check will fail because `fn` is already a `Type`
    - `if _firstcall` is `False`
    - Numba raises a `NotImplementedError` which is not caught in the new exception model

Notice that Numba never tried the original inputs (`fn`) after the `self.refresh()` call, but I believe it should. 

Below is a minimum viable reproducer that declares and calls `lround` from the cmath library:

```python
from numba.core.typing.templates import AbstractTemplate, infer_global
from numba.core.imputils import lower_builtin
from numba.core import funcdesc, types
from numba import njit

def lround(a):
    pass

@infer_global(lround)
class lroundTemplate(AbstractTemplate):
    key = lround

    def generic(self, args, kws):
        signature = types.int64(types.float64)

        # insert a new builtin during the compilation process
        @lower_builtin(lround, types.float64)
        def codegen(context, builder, sig, args):
            fndesc = funcdesc.ExternalFunctionDescriptor('lround', sig.return_type, sig.args)
            func = context.declare_external_function(builder.module, fndesc)
            return builder.call(func, args)

        return signature

@njit('int64(float64)')
def foo(a):
    return lround(a)

print(foo(3.4))
```

Although the test reproduces the error in Numba master, I do not expect it to be fixed. This model of declaring new builtins during the compilation is not common (at least I've never seen it before) in the Numba codebase and probably will not affect any user of Numba. I use it in [RBC](https://github.com/xnd-project/rbc/) to postpone the definition of some types until there is information from the server for cross-compilation (i.e. `int` bit width depends on the architecture). I am reporting this issue in case someone else faces the same thing.
> Notice that Numba never tried the original inputs (`fn`) after the `self.refresh()` call, but I believe it should.

My first impression is that this makes sense.

> Although the test reproduces the error in Numba master, I do not expect it to be fixed.

Shouldn't we fix it, if it should try the inputs after refreshing?

> This model of declaring new builtins during the compilation is not common (at least I've never seen it before) in the Numba codebase and probably will not affect any user of Numba.

It might not be common, but I do feel like it should work - one reason I'd be interested in it working is that it would have to be supported by something that's a bit more straightforward than the way things work now (which I admit to not fully understanding how the registries are actually working).
Thanks for debugging this @guilhermeleobas, much appreciated. I think the fix is to raise the numba internal exception type in the first round of calling the function and then to raise the hard error in the recursion? I also think this function would probably benefit from a bit of a refactor and an update with respect to recent internal changes to registries etc.
> Thanks for debugging this @guilhermeleobas, much appreciated. I think the fix is to raise the numba internal exception type in the first round of calling the function and then to raise the hard error in the recursion? I also think this function would probably benefit from a bit of a refactor and an update with respect to recent internal changes to registries etc.

This unfortunately didn't work.

Also, declaring and compiling new functions during typing is more rare using the lower level APIs, however, I think it probably ought to be fixed.
Debugging this a bit... applying this patch to 0.54:
```patch
diff --git a/numba/core/base.py b/numba/core/base.py
index 830ccc0..3ceb0ac 100644
--- a/numba/core/base.py
+++ b/numba/core/base.py
@@ -539,34 +539,47 @@ class BaseContext(object):
         Return the implementation of function *fn* for signature *sig*.
         The return value is a callable with the signature (builder, args).
         """
+        def printit(where, firstcall, func):
+            fmt = '{:<25} {:<5} {:<40} {:<40}'
+            print(fmt.format(where, str(firstcall), str(func), str(type(func))))
         assert sig is not None
         sig = sig.as_function()
         if isinstance(fn, (types.Function, types.BoundFunction,
                            types.Dispatcher)):
+            printit("1 Branch", _firstcall, fn)
             key = fn.get_impl_key(sig)
             overloads = self._defns[key]
         else:
+            printit("2 Branch", _firstcall, fn)
             key = fn
             overloads = self._defns[key]
 
         try:
+            printit("3 Try", _firstcall, fn)
             return _wrap_impl(overloads.find(sig.args), self, sig)
         except NotImplementedError:
+            printit("4 Except", _firstcall, fn)
             pass
         if isinstance(fn, types.Type):
+            printit("5 Branch", _firstcall, fn)
             # It's a type instance => try to find a definition for the type class
             try:
+                printit("6 Try recurse", _firstcall, fn)
                 return self.get_function(type(fn), sig)
             except NotImplementedError:
+                printit("7 Except", _firstcall, fn)
                 # Raise exception for the type instance, for a better error message
                 pass
 
+        printit("8 Fall through", _firstcall, fn)
         # Automatically refresh the context to load new registries if we are
         # calling the first time.
         if _firstcall:
+            printit("9 Branch refresh recurse", _firstcall, fn)
             self.refresh()
             return self.get_function(fn, sig, _firstcall=False)
 
+        printit("10 Fall through raise", _firstcall, fn)
         raise NotImplementedError("No definition for lowering %s%s" % (key, sig))
```
and running the reproducer gives:
```
$ python issue7507.py 
1 Branch                  True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
3 Try                     True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
4 Except                  True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
5 Branch                  True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
6 Try recurse             True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
2 Branch                  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
3 Try                     True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
4 Except                  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
8 Fall through            True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
9 Branch refresh recurse  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
2 Branch                  False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
3 Try                     False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
4 Except                  False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
8 Fall through            False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
10 Fall through raise     False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
7 Except                  True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
8 Fall through            True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
9 Branch refresh recurse  True  Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
1 Branch                  False Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
3 Try                     False Function(<function lround at 0x7f65f5c61310>) <class 'numba.core.types.functions.Function'>
3
```
Applying this patch to mainline:
```diff
diff --git a/numba/core/base.py b/numba/core/base.py
index e3b55c0..7891ca6 100644
--- a/numba/core/base.py
+++ b/numba/core/base.py
@@ -539,34 +539,47 @@ class BaseContext(object):
         Return the implementation of function *fn* for signature *sig*.
         The return value is a callable with the signature (builder, args).
         """
+        def printit(where, firstcall, func):
+            fmt = '{:<25} {:<5} {:<40} {:<40}'
+            print(fmt.format(where, str(firstcall), str(func), str(type(func))))
         assert sig is not None
         sig = sig.as_function()
         if isinstance(fn, (types.Function, types.BoundFunction,
                            types.Dispatcher)):
+            printit("1 Branch", _firstcall, fn)
             key = fn.get_impl_key(sig)
             overloads = self._defns[key]
         else:
+            printit("2 Branch", _firstcall, fn)
             key = fn
             overloads = self._defns[key]
 
         try:
+            printit("3 Try", _firstcall, fn)
             return _wrap_impl(overloads.find(sig.args), self, sig)
         except errors.NumbaNotImplementedError:
+            printit("4 Except", _firstcall, fn)
             pass
         if isinstance(fn, types.Type):
+            printit("5 Branch", _firstcall, fn)
             # It's a type instance => try to find a definition for the type class
             try:
+                printit("6 Try recurse", _firstcall, fn)
                 return self.get_function(type(fn), sig)
             except errors.NumbaNotImplementedError:
+                printit("7 Except", _firstcall, fn)
                 # Raise exception for the type instance, for a better error message
                 pass
 
+        printit("8 Fall through", _firstcall, fn)
         # Automatically refresh the context to load new registries if we are
         # calling the first time.
         if _firstcall:
+            printit("9 Branch refresh recurse", _firstcall, fn)
             self.refresh()
             return self.get_function(fn, sig, _firstcall=False)
 
+        printit("10 Fall through raise", _firstcall, fn)
         raise NotImplementedError("No definition for lowering %s%s" % (key, sig))
```
gives:
```
$ NUMBA_CAPTURED_ERRORS='old_style' python issue7507.py 
1 Branch                  True  Function(<function lround at 0x7f83e61be310>) <class 'numba.core.types.functions.Function'>
3 Try                     True  Function(<function lround at 0x7f83e61be310>) <class 'numba.core.types.functions.Function'>
4 Except                  True  Function(<function lround at 0x7f83e61be310>) <class 'numba.core.types.functions.Function'>
5 Branch                  True  Function(<function lround at 0x7f83e61be310>) <class 'numba.core.types.functions.Function'>
6 Try recurse             True  Function(<function lround at 0x7f83e61be310>) <class 'numba.core.types.functions.Function'>
2 Branch                  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
3 Try                     True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
4 Except                  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
8 Fall through            True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
9 Branch refresh recurse  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
2 Branch                  False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
3 Try                     False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
4 Except                  False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
8 Fall through            False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
10 Fall through raise     False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
...
numba/core/base.py", line 583, in get_function
    raise NotImplementedError("No definition for lowering %s%s" % (key, sig))
numba.core.errors.LoweringError: Failed in nopython mode pipeline (step: native lowering)
No definition for lowering <class 'numba.core.types.functions.Function'>(float64,) -> int64

File "issue7507.py", line 27:
def foo(a):
    return lround(a)
    ^
```
and:
```
$ NUMBA_CAPTURED_ERRORS='new_style' python issue7507.py 
1 Branch                  True  Function(<function lround at 0x7fe45ff00310>) <class 'numba.core.types.functions.Function'>
3 Try                     True  Function(<function lround at 0x7fe45ff00310>) <class 'numba.core.types.functions.Function'>
4 Except                  True  Function(<function lround at 0x7fe45ff00310>) <class 'numba.core.types.functions.Function'>
5 Branch                  True  Function(<function lround at 0x7fe45ff00310>) <class 'numba.core.types.functions.Function'>
6 Try recurse             True  Function(<function lround at 0x7fe45ff00310>) <class 'numba.core.types.functions.Function'>
2 Branch                  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
3 Try                     True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
4 Except                  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
8 Fall through            True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
9 Branch refresh recurse  True  <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
2 Branch                  False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
3 Try                     False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
4 Except                  False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
8 Fall through            False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
10 Fall through raise     False <class 'numba.core.types.functions.Function'> <class 'numba.core.types.abstract._TypeMetaclass'>
...
numba/core/base.py", line 583, in get_function
    raise NotImplementedError("No definition for lowering %s%s" % (key, sig))
NotImplementedError: No definition for lowering <class 'numba.core.types.functions.Function'>(float64,) -> int64
```
which I think confirms this issue is invariant of the error handler.
I'm unconvinced that there's a noninvasive way to fix this whilst supporting the new error model behaviours.

I'm also unconvinced that there's a way to fix the current logic that doesn't involve turning the fall through `NotImplementedError` into a `errors.NumbaNotImplementedError`. The knock-on effect doing this is that there will be no "hard error" in the new error model and instead a standard `LoweringError` would be raised with this error captured inside it. This is not ideal but is probably acceptable as this is about finding a lowering implementation opposed to dealing with a problem in compiling one. Further the error information is not lost, it's just wrapped, which was in part the aim of the new error handling behaviours (information preservation).

I think this problem in general stems from using exceptions for control flow purposes and the fall through raising of an exception being used as part of that control flow in a valid case (the one above). That there's multiple recursion points also makes this harder to debug/understand.

The other option to fix this is to rewrite this logic to match what is needed for the current Numba code base (IIRC this function has not been updated for a long while, whilst most of the code that uses it has changed drastically). This may well be a better option in the long term albeit potentially a lot of effort.

Resolving patch:
diff --git a/numba/core/base.py b/numba/core/base.py
--- a/numba/core/base.py
+++ b/numba/core/base.py
@@ -560,7 +560,7 @@ def get_function(self, fn, sig, _firstcall=True):
             # It's a type instance => try to find a definition for the type class
             try:
                 return self.get_function(type(fn), sig)
-            except errors.NumbaNotImplementedError:
+            except NotImplementedError:
                 # Raise exception for the type instance, for a better error message
                 pass
 


Test patch:
diff --git a/numba/tests/test_practical_lowering_issues.py b/numba/tests/test_practical_lowering_issues.py
--- a/numba/tests/test_practical_lowering_issues.py
+++ b/numba/tests/test_practical_lowering_issues.py
@@ -12,6 +12,11 @@
 from numba.tests.support import MemoryLeakMixin, TestCase
 
 
+def issue7507_lround(a):
+    """Dummy function used in test"""
+    pass
+
+
 class TestLowering(MemoryLeakMixin, TestCase):
     def test_issue4156_loop_vars_leak(self):
         """Test issues with zero-filling of refct'ed variables inside loops.
@@ -175,3 +180,33 @@ def foo(arr):
         got = foo(arr)
         expect = foo.py_func(arr)
         self.assertEqual(got, expect)
+
+    def test_issue7507(self):
+        """
+        Test a problem with BaseContext.get_function() because of changes
+        related to the new style error handling.
+        """
+        from numba.core.typing.templates import AbstractTemplate, infer_global
+        from numba.core.imputils import lower_builtin
+
+        @infer_global(issue7507_lround)
+        class lroundTemplate(AbstractTemplate):
+            key = issue7507_lround
+
+            def generic(self, args, kws):
+                signature = types.int64(types.float64)
+
+                # insert a new builtin during the compilation process
+                @lower_builtin(issue7507_lround, types.float64)
+                def codegen(context, builder, sig, args):
+                    # Simply truncate with the cast to integer.
+                    return context.cast(builder, args[0], sig.args[0],
+                                        sig.return_type)
+
+                return signature
+
+        @njit('int64(float64)')
+        def foo(a):
+            return issue7507_lround(a)
+
+        self.assertEqual(foo(3.4), 3)