Repository: numba/numba

Issue:
debuginfo flag attempts to set noinline on functions with alwaysinline
<!--

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'.

This (and in fact any function implemented via the `@overload_glue` wrappers):
```python
from numba import njit
import numpy as np

@njit
def foo():
    a = np.zeros(1)

print(foo())
```
will not compile if `NUMBA_DEBUGINFO=1` is set. End of the stack trace:
```
  File "<numba_path>/numba/core/lowering.py", line 255, in lower_function_body
    self.pre_lower()
  File "<numba_path>/numba/core/lowering.py", line 133, in pre_lower
    self.debuginfo.mark_subprogram(function=self.builder.function,
  File "<numba_path>/numba/core/debuginfo.py", line 319, in mark_subprogram
    function.attributes.add('noinline')
  File "<llvmlite_path>/llvmlite/ir/values.py", line 887, in add
    raise ValueError("Can't have alwaysinline and noinline")
ValueError: Can't have alwaysinline and noinline
```
this comes from the `overload_glue` functions being forced as `alwaysinline` and the `debuginfo` flag trying to prevent inlining.


Discussion:
NOTE: Need to fix this for 0.55 as it's a regression.

Resolving patch:
diff --git a/numba/core/debuginfo.py b/numba/core/debuginfo.py
--- a/numba/core/debuginfo.py
+++ b/numba/core/debuginfo.py
@@ -315,8 +315,11 @@ def mark_subprogram(self, function, qualname, argnames, argtypes, line):
                                        line=line, function=function,
                                        argmap=argmap)
         function.set_metadata("dbg", di_subp)
-        # disable inlining for this function for easier debugging
-        function.attributes.add('noinline')
+
+        # Don't marked alwaysinline functions as noinline.
+        if 'alwaysinline' not in function.attributes:
+            # disable inlining for this function for easier debugging
+            function.attributes.add('noinline')
 
     def finalize(self):
         dbgcu = cgutils.get_or_insert_named_metadata(self.module, self.DBG_CU_NAME)


Test patch:
diff --git a/numba/tests/test_debuginfo.py b/numba/tests/test_debuginfo.py
--- a/numba/tests/test_debuginfo.py
+++ b/numba/tests/test_debuginfo.py
@@ -2,6 +2,7 @@
 import inspect
 import re
 import numpy as np
+import math
 
 from numba.tests.support import TestCase, override_config, needs_subprocess
 from numba import jit, njit
@@ -58,6 +59,62 @@ def bar(x):
                 return x
             self._check(bar, sig=(types.int32,), expect=False)
 
+    def test_llvm_inliner_flag_conflict(self):
+        # bar will be marked as 'alwaysinline', but when DEBUGINFO_DEFAULT is
+        # set functions are marked as 'noinline' this results in a conflict.
+        # baz will be marked as 'noinline' as a result of DEBUGINFO_DEFAULT
+
+        @njit(forceinline=True)
+        def bar(x):
+            return math.sin(x)
+
+        @njit(forceinline=False)
+        def baz(x):
+            return math.cos(x)
+
+        @njit
+        def foo(x):
+            a = bar(x)
+            b = baz(x)
+            return a, b
+
+        # check it compiles
+        with override_config('DEBUGINFO_DEFAULT', 1):
+            result = foo(np.pi)
+
+        self.assertPreciseEqual(result, foo.py_func(np.pi))
+
+        # check the LLVM IR has bar marked as 'alwaysinline' and baz as noinline
+        full_ir = foo.inspect_llvm(foo.signatures[0])
+        module = llvm.parse_assembly(full_ir)
+        name = foo.overloads[foo.signatures[0]].fndesc.mangled_name
+        funcs = [x for x in module.functions if x.name == name]
+        self.assertEqual(len(funcs), 1)
+        func = funcs[0]
+
+        # find the function calls and save the associated statements
+        f_names = []
+        for blk in func.blocks:
+            for stmt in blk.instructions:
+                if stmt.opcode == 'call':
+                    # stmt.function.name  This is the function being called
+                    f_names.append(str(stmt).strip())
+
+        # Need to check there's two specific things in the calls in the IR
+        # 1. a call to the llvm.sin.f64 intrinsic, this is from the inlined bar
+        # 2. a call to the baz function, this is from the noinline baz
+        found_sin = False
+        found_baz = False
+        baz_name = baz.overloads[baz.signatures[0]].fndesc.mangled_name
+        for x in f_names:
+            if not found_sin and re.match('.*llvm.sin.f64.*', x):
+                found_sin = True
+            if not found_baz and re.match(f'.*{baz_name}.*', x):
+                found_baz = True
+
+        self.assertTrue(found_sin)
+        self.assertTrue(found_baz)
+
 
 class TestDebugInfoEmission(TestCase):
     """ Tests that debug info is emitted correctly.