Repository: numba/numba

Issue:
graphviz/dot internal buffer truncation causes issues for `inspect_cfg`.
- [x] I have tried using the 0.52.0rc1 ~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 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).

This
```python
from numba import njit, types
from numba.experimental import jitclass
import numpy as np
import string

@jitclass({x:types.int64 for x in string.ascii_letters})
class Foo():
    def __init__(self):
        pass
@njit
def bar():
    a = Foo()

bar()

bar.inspect_cfg(bar.signatures[0]).display(view=True)
```
creates a massive mangled name for `Foo` which then causes graphviz/dot problems later when trying to render the CFG.
```
graphviz.backend.CalledProcessError: Command '['dot', '-Tpdf', '-O', '_ZN8__main__7bar$241E.gv']' returned non-zero exit status 1. [stderr: b'Error: not well-formed (invalid token) in line 1 \n... ... :int64,O:int64,P:int64,Q:int64,R:int64,S:int64,T:int64,U:int64,V:int64,W:int64,X:int64,Y:int64,Z:int64&#92;&gt;&quot</td> ...\nin label of node Node0x555722175a90\n']
```
if this is truncated, it can also cause problems when trying to render the CFG as the filename is based on the graph name, which is massive, and results in an `OSError` as maximum filename length is exceeded.


Discussion:

Resolving patch:
diff --git a/numba/core/codegen.py b/numba/core/codegen.py
--- a/numba/core/codegen.py
+++ b/numba/core/codegen.py
@@ -95,9 +95,10 @@ def __init__(self, cres, name, py_func, **kwargs):
         self.dot = ll.get_function_cfg(fn)
         self.kwargs = kwargs
 
-    def pretty_printer(self, filename=None, view=None, highlight=True,
-                      interleave=False, strip_ir=False, show_key=True,
-                      fontsize=10):
+    def pretty_printer(self, filename=None, view=None, render_format=None,
+                       highlight=True,
+                       interleave=False, strip_ir=False, show_key=True,
+                       fontsize=10):
         """
         "Pretty" prints the DOT graph of the CFG.
         For explanation of the parameters see the docstring for
@@ -118,7 +119,9 @@ def pretty_printer(self, filename=None, view=None, highlight=True,
                                     returns=_default,
                                     raises=_default,
                                     meminfo=_default,
-                                    branches=_default)
+                                    branches=_default,
+                                    llvm_intrin_calls=_default,
+                                    function_calls=_default,)
         _interleave = SimpleNamespace(python=_default, lineinfo=_default)
 
         def parse_config(_config, kwarg):
@@ -163,6 +166,8 @@ def parse_config(_config, kwarg):
         cs['raise'] = 'lightpink'
         cs['meminfo'] = 'lightseagreen'
         cs['return'] = 'purple'
+        cs['llvm_intrin_calls'] = 'rosybrown'
+        cs['function_calls'] = 'tomato'
 
         # Get the raw dot format information from LLVM and the LLVM IR
         fn = self.cres.get_function(self.name)
@@ -191,7 +196,16 @@ def init_digraph(name, fname, fontsize):
             # name and fname are arbitrary graph and file names, they appear in
             # some rendering formats, the fontsize determines the output
             # fontsize.
-            f = gv.Digraph(name, fname)
+
+            # truncate massive mangled names as file names as it causes OSError
+            # when trying to render to pdf
+            cmax = 200
+            if len(fname) > cmax:
+                wstr = (f'CFG output filname "{fname}" exceeds maximum '
+                        f'supported length, it will be truncated.')
+                warnings.warn(wstr, NumbaInvalidConfigWarning)
+                fname = fname[:cmax]
+            f = gv.Digraph(name, filename=fname)
             f.attr(rankdir='TB')
             f.attr('node', shape='none', fontsize='%s' % str(fontsize))
             return f
@@ -217,8 +231,10 @@ def init_digraph(name, fname, fontsize):
         nrt_incref = re.compile(r"@NRT_incref\b")
         nrt_decref = re.compile(r"@NRT_decref\b")
         nrt_meminfo = re.compile("@NRT_MemInfo")
-        ll_raise = re.compile("ret i32 1,")
-        ll_return = re.compile("ret i32 [^1],")
+        ll_intrin_calls = re.compile(r".*call.*@llvm\..*")
+        ll_function_call = re.compile(r".*call.*@.*")
+        ll_raise = re.compile("ret i32.*\!ret_is_raise.*")
+        ll_return = re.compile("ret i32 [^1],?.*")
 
         # wrapper function for line wrapping LLVM lines
         def wrap(s):
@@ -227,12 +243,23 @@ def wrap(s):
         # function to fix (sometimes escaped for DOT!) LLVM IR etc that needs to
         # be HTML escaped
         def clean(s):
+            # Grab first 300 chars only, 1. this should be enough to identify
+            # the token and it keeps names short. 2. graphviz/dot has a maximum
+            # buffer size near 585?!, with additional transforms it's hard to
+            # know if this would be exceeded. 3. hash of the token string is
+            # written into the rendering to permit exact identification against
+            # e.g. LLVM IR dump if necessary.
+            n = 300
+            if len(s) > n:
+                hs = str(hash(s))
+                s = '{}...<hash={}>'.format(s[:n], hs)
             s = html.escape(s) # deals with  &, < and >
             s = s.replace('\\{', "&#123;")
             s = s.replace('\\}', "&#125;")
             s = s.replace('\\', "&#92;")
             s = s.replace('%', "&#37;")
-            return s.replace('!', "&#33;")
+            s = s.replace('!', "&#33;")
+            return s
 
         # These hold the node and edge ids from the raw dot information. They
         # are used later to wire up a new DiGraph that has the same structure
@@ -399,6 +426,10 @@ def metadata_interleave(l, new_lines):
                     colour = cs['raise']
                 elif _highlight.returns and ll_return.search(l):
                     colour = cs['return']
+                elif _highlight.llvm_intrin_calls and ll_intrin_calls.search(l):
+                    colour = cs['llvm_intrin_calls']
+                elif _highlight.function_calls and ll_function_call.search(l):
+                    colour = cs['function_calls']
                 else:
                     colour = cs['default']
 
@@ -457,21 +488,23 @@ def metadata_interleave(l, new_lines):
 
         # Render if required
         if filename is not None or view is not None:
-            f.render(filename=filename, view=view, format='pdf')
+            f.render(filename=filename, view=view, format=render_format)
 
         # Else pipe out a SVG
         return f.pipe(format='svg')
 
-    def display(self, filename=None, view=False):
+    def display(self, filename=None, format='pdf', view=False):
         """
         Plot the CFG.  In IPython notebook, the return image object can be
         inlined.
 
         The *filename* option can be set to a specific path for the rendered
         output to write to.  If *view* option is True, the plot is opened by
-        the system default application for the image format (PDF).
+        the system default application for the image format (PDF). *format* can
+        be any valid format string accepted by graphviz, default is 'pdf'.
         """
-        rawbyt = self.pretty_printer(filename=filename, view=view, **self.kwargs)
+        rawbyt = self.pretty_printer(filename=filename, view=view,
+                                     render_format=format, **self.kwargs)
         return rawbyt.decode('utf-8')
 
     def _repr_svg_(self):


Test patch: