Skip to content
This repository was archived by the owner on Jan 25, 2023. It is now read-only.

Commit 0b95407

Browse files
authored
Fix various issues with dwarf emission (numba#7116)
1 parent ddf817a commit 0b95407

4 files changed

Lines changed: 149 additions & 4 deletions

File tree

‎numba/core/cgutils.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,13 +370,20 @@ def alloca_once(builder, ty, size=None, name='', zfill=False):
370370
"""
371371
if isinstance(size, int):
372372
size = ir.Constant(intp_t, size)
373+
# suspend dwarf emission else it links up python source lines with alloca in
374+
# the entry block as well as their actual location and it makes the debug
375+
# info "jump about".
376+
ref = builder.debug_metadata
377+
builder.debug_metadata = None
373378
with builder.goto_entry_block():
374379
ptr = builder.alloca(ty, size=size, name=name)
375380
# Always zero-fill at init-site. This is safe.
376381
builder.store(ptr.type.pointee(None), ptr)
377382
# Also zero-fill at the use-site
378383
if zfill:
379384
builder.store(ptr.type.pointee(None), ptr)
385+
# continue with dwarf emission
386+
builder.debug_metadata = ref
380387
return ptr
381388

382389

‎numba/core/debuginfo.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def _di_file(self):
215215

216216
def _di_compile_unit(self):
217217
return self.module.add_debug_info('DICompileUnit', {
218-
'language': ir.DIToken('DW_LANG_Python'),
218+
'language': ir.DIToken('DW_LANG_C_plus_plus'),
219219
'file': self.difile,
220220
'producer': 'Numba',
221221
'runtimeVersion': 0,

‎numba/core/lowering.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,11 +1302,15 @@ def alloca_lltype(self, name, lltype):
13021302
aptr = cgutils.alloca_once(self.builder, lltype,
13031303
name=name, zfill=False)
13041304
if is_uservar:
1305+
# If it's an arg set the location as the function definition line
1306+
if name in self.func_ir.arg_names:
1307+
loc = self.loc.with_lineno(self.func_ir.loc.line)
1308+
else:
1309+
loc = self.loc
13051310
# Emit debug info for user variable
13061311
sizeof = self.context.get_abi_sizeof(lltype)
13071312
self.debuginfo.mark_variable(self.builder, aptr, name=name,
1308-
lltype=lltype, size=sizeof,
1309-
loc=self.loc)
1313+
lltype=lltype, size=sizeof, loc=loc)
13101314
return aptr
13111315

13121316
def incref(self, typ, val):

‎numba/tests/test_debuginfo.py‎

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import re
2+
import inspect
23

34
from numba.tests.support import TestCase, override_config
4-
from numba import jit
5+
from numba import jit, njit
56
from numba.core import types
67
import unittest
8+
import llvmlite.binding as llvm
79

810

911
class TestDebugInfo(TestCase):
1012
"""
1113
These tests only checks the compiled assembly for debuginfo.
1214
"""
15+
1316
def _getasm(self, fn, sig):
1417
fn.compile(sig)
1518
return fn.inspect_asm(sig)
@@ -49,5 +52,136 @@ def bar(x):
4952
self._check(bar, sig=(types.int32,), expect=False)
5053

5154

55+
class TestDebugInfoEmission(TestCase):
56+
""" Tests that debug info is emitted correctly.
57+
"""
58+
59+
def _get_llvmir(self, fn, sig):
60+
with override_config('OPT', 0):
61+
fn.compile(sig)
62+
return fn.inspect_llvm(sig)
63+
64+
def _get_metadata(self, fn, sig):
65+
ll = self._get_llvmir(fn, sig).splitlines()
66+
meta_re = re.compile(r'![0-9]+ =.*')
67+
metadata = []
68+
for line in ll:
69+
if meta_re.match(line):
70+
metadata.append(line)
71+
return metadata
72+
73+
def test_DW_LANG(self):
74+
75+
@njit(debug=True)
76+
def foo():
77+
pass
78+
79+
metadata = self._get_metadata(foo, sig=())
80+
DICompileUnit = metadata[0]
81+
self.assertEqual('!0', DICompileUnit[:2])
82+
self.assertIn('!DICompileUnit(language: DW_LANG_C_plus_plus',
83+
DICompileUnit)
84+
self.assertIn('producer: "Numba"', DICompileUnit)
85+
86+
def test_DILocation(self):
87+
""" Tests that DILocation information is reasonable.
88+
"""
89+
90+
@njit(debug=True, error_model='numpy')
91+
def foo(a):
92+
b = a + 1.23
93+
c = a * 2.34
94+
d = b / c
95+
print(d)
96+
return d
97+
98+
# the above produces LLVM like:
99+
# define function() {
100+
# entry:
101+
# alloca
102+
# store 0 to alloca
103+
# <arithmetic for doing the operations on b, c, d>
104+
# setup for print
105+
# branch
106+
# other_labels:
107+
# ... <elided>
108+
# }
109+
#
110+
# The following checks that:
111+
# * the alloca and store have no !dbg
112+
# * the arithmetic occurs in the order defined and with !dbg
113+
# * that the !dbg entries are monotonically increasing in value with
114+
# source line number
115+
116+
metadata = self._get_metadata(foo, sig=(types.float64,))
117+
full_ir = self._get_llvmir(foo, sig=(types.float64,))
118+
119+
llvm.initialize()
120+
llvm.initialize_native_target()
121+
llvm.initialize_native_asmprinter()
122+
123+
module = llvm.parse_assembly(full_ir)
124+
125+
name = foo.overloads[foo.signatures[0]].fndesc.mangled_name
126+
funcs = [x for x in module.functions if x.name == name]
127+
self.assertEqual(len(funcs), 1)
128+
func = funcs[0]
129+
blocks = [x for x in func.blocks]
130+
self.assertGreater(len(blocks), 1)
131+
block = blocks[0]
132+
133+
# Find non-call instr and check the sequence is as expected
134+
instrs = [x for x in block.instructions if x.opcode != 'call']
135+
op_seq = [x.opcode for x in instrs]
136+
op_expect = ('fadd', 'fmul', 'fdiv')
137+
self.assertIn(''.join(op_expect), ''.join(op_seq))
138+
139+
# Parse out metadata from end of each line, check it monotonically
140+
# ascends with LLVM source line. Also store all the dbg references,
141+
# these will be checked later.
142+
line2dbg = set()
143+
re_dbg_ref = re.compile(r'.*!dbg (![0-9]+).*$')
144+
found = -1
145+
for instr in instrs:
146+
inst_as_str = str(instr)
147+
matched = re_dbg_ref.match(inst_as_str)
148+
if not matched:
149+
# if there's no match, ensure it is one of alloca or store,
150+
# it's important that the zero init/alloca instructions have
151+
# no dbg data
152+
accepted = ('alloca ', 'store ')
153+
self.assertTrue(any([x in inst_as_str for x in accepted]))
154+
continue
155+
groups = matched.groups()
156+
self.assertEqual(len(groups), 1)
157+
dbg_val = groups[0]
158+
int_dbg_val = int(dbg_val[1:])
159+
if found >= 0:
160+
self.assertTrue(int_dbg_val >= found)
161+
found = int_dbg_val
162+
# some lines will alias dbg info, this is fine, it's only used to
163+
# make sure that the line numbers are correct WRT python
164+
line2dbg.add(dbg_val)
165+
166+
pysrc, pysrc_line_start = inspect.getsourcelines(foo)
167+
168+
# build a map of dbg reference to DI* information
169+
metadata_definition_map = dict()
170+
meta_definition_split = re.compile(r'(![0-9]+) = (.*)')
171+
for line in metadata:
172+
matched = meta_definition_split.match(line)
173+
if matched:
174+
dbg_val, info = matched.groups()
175+
metadata_definition_map[dbg_val] = info
176+
177+
# Pull out metadata entries referred to by the llvm line end !dbg
178+
# check they match the python source, the +2 is for the @njit decorator
179+
# and the function definition line.
180+
pyln_range = range(pysrc_line_start + 2, pysrc_line_start + len(pysrc))
181+
for (k, line_no) in zip(sorted(line2dbg), pyln_range):
182+
dilocation_info = metadata_definition_map[k]
183+
self.assertIn(f'line: {line_no}', dilocation_info)
184+
185+
52186
if __name__ == '__main__':
53187
unittest.main()

0 commit comments

Comments
 (0)