xref: /llvm-project/clang/utils/analyzer/exploded-graph-rewriter.py (revision 48a5c83af44235130494fce4e7e08d7128475414)
1#!/usr/bin/env python
2#
3#===- exploded-graph-rewriter.py - ExplodedGraph dump tool -----*- python -*--#
4#
5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6# See https://llvm.org/LICENSE.txt for license information.
7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8#
9#===-----------------------------------------------------------------------===#
10
11
12from __future__ import print_function
13
14import argparse
15import collections
16import difflib
17import json
18import logging
19import re
20
21
22# A helper function for finding the difference between two dictionaries.
23def diff_dicts(curr, prev):
24    removed = [k for k in prev if k not in curr or curr[k] != prev[k]]
25    added = [k for k in curr if k not in prev or curr[k] != prev[k]]
26    return (removed, added)
27
28
29# Represents any program state trait that is a dictionary of key-value pairs.
30class GenericMap(object):
31    def __init__(self, items):
32        self.generic_map = collections.OrderedDict(items)
33
34    def diff(self, prev):
35        return diff_dicts(self.generic_map, prev.generic_map)
36
37    def is_different(self, prev):
38        removed, added = self.diff(prev)
39        return len(removed) != 0 or len(added) != 0
40
41
42# A deserialized source location.
43class SourceLocation(object):
44    def __init__(self, json_loc):
45        super(SourceLocation, self).__init__()
46        self.line = json_loc['line']
47        self.col = json_loc['column']
48        self.filename = json_loc['filename'] \
49            if 'filename' in json_loc else '(main file)'
50
51
52# A deserialized program point.
53class ProgramPoint(object):
54    def __init__(self, json_pp):
55        super(ProgramPoint, self).__init__()
56        self.kind = json_pp['kind']
57        self.tag = json_pp['tag']
58        if self.kind == 'Edge':
59            self.src_id = json_pp['src_id']
60            self.dst_id = json_pp['dst_id']
61        elif self.kind == 'Statement':
62            self.stmt_kind = json_pp['stmt_kind']
63            self.stmt_point_kind = json_pp['stmt_point_kind']
64            self.pointer = json_pp['pointer']
65            self.pretty = json_pp['pretty']
66            self.loc = SourceLocation(json_pp['location']) \
67                if json_pp['location'] is not None else None
68        elif self.kind == 'BlockEntrance':
69            self.block_id = json_pp['block_id']
70
71
72# A single expression acting as a key in a deserialized Environment.
73class EnvironmentBindingKey(object):
74    def __init__(self, json_ek):
75        super(EnvironmentBindingKey, self).__init__()
76        # CXXCtorInitializer is not a Stmt!
77        self.stmt_id = json_ek['stmt_id'] if 'stmt_id' in json_ek \
78            else json_ek['init_id']
79        self.pretty = json_ek['pretty']
80        self.kind = json_ek['kind'] if 'kind' in json_ek else None
81
82    def _key(self):
83        return self.stmt_id
84
85    def __eq__(self, other):
86        return self._key() == other._key()
87
88    def __hash__(self):
89        return hash(self._key())
90
91
92# Deserialized description of a location context.
93class LocationContext(object):
94    def __init__(self, json_frame):
95        super(LocationContext, self).__init__()
96        self.lctx_id = json_frame['lctx_id']
97        self.caption = json_frame['location_context']
98        self.decl = json_frame['calling']
99        self.line = json_frame['call_line']
100
101    def _key(self):
102        return self.lctx_id
103
104    def __eq__(self, other):
105        return self._key() == other._key()
106
107    def __hash__(self):
108        return hash(self._key())
109
110
111# A group of deserialized Environment bindings that correspond to a specific
112# location context.
113class EnvironmentFrame(object):
114    def __init__(self, json_frame):
115        super(EnvironmentFrame, self).__init__()
116        self.location_context = LocationContext(json_frame)
117        self.bindings = collections.OrderedDict(
118            [(EnvironmentBindingKey(b),
119              b['value']) for b in json_frame['items']]
120            if json_frame['items'] is not None else [])
121
122    def diff_bindings(self, prev):
123        return diff_dicts(self.bindings, prev.bindings)
124
125    def is_different(self, prev):
126        removed, added = self.diff_bindings(prev)
127        return len(removed) != 0 or len(added) != 0
128
129
130# A deserialized Environment. This class can also hold other entities that
131# are similar to Environment, such as Objects Under Construction.
132class GenericEnvironment(object):
133    def __init__(self, json_e):
134        super(GenericEnvironment, self).__init__()
135        self.frames = [EnvironmentFrame(f) for f in json_e]
136
137    def diff_frames(self, prev):
138        # TODO: It's difficult to display a good diff when frame numbers shift.
139        if len(self.frames) != len(prev.frames):
140            return None
141
142        updated = []
143        for i in range(len(self.frames)):
144            f = self.frames[i]
145            prev_f = prev.frames[i]
146            if f.location_context == prev_f.location_context:
147                if f.is_different(prev_f):
148                    updated.append(i)
149            else:
150                # We have the whole frame replaced with another frame.
151                # TODO: Produce a nice diff.
152                return None
153
154        # TODO: Add support for added/removed.
155        return updated
156
157    def is_different(self, prev):
158        updated = self.diff_frames(prev)
159        return updated is None or len(updated) > 0
160
161
162# A single binding key in a deserialized RegionStore cluster.
163class StoreBindingKey(object):
164    def __init__(self, json_sk):
165        super(StoreBindingKey, self).__init__()
166        self.kind = json_sk['kind']
167        self.offset = json_sk['offset']
168
169    def _key(self):
170        return (self.kind, self.offset)
171
172    def __eq__(self, other):
173        return self._key() == other._key()
174
175    def __hash__(self):
176        return hash(self._key())
177
178
179# A single cluster of the deserialized RegionStore.
180class StoreCluster(object):
181    def __init__(self, json_sc):
182        super(StoreCluster, self).__init__()
183        self.base_region = json_sc['cluster']
184        self.bindings = collections.OrderedDict(
185            [(StoreBindingKey(b), b['value']) for b in json_sc['items']])
186
187    def diff_bindings(self, prev):
188        return diff_dicts(self.bindings, prev.bindings)
189
190    def is_different(self, prev):
191        removed, added = self.diff_bindings(prev)
192        return len(removed) != 0 or len(added) != 0
193
194
195# A deserialized RegionStore.
196class Store(object):
197    def __init__(self, json_s):
198        super(Store, self).__init__()
199        self.ptr = json_s['pointer']
200        self.clusters = collections.OrderedDict(
201            [(c['pointer'], StoreCluster(c)) for c in json_s['items']])
202
203    def diff_clusters(self, prev):
204        removed = [k for k in prev.clusters if k not in self.clusters]
205        added = [k for k in self.clusters if k not in prev.clusters]
206        updated = [k for k in prev.clusters if k in self.clusters
207                   and prev.clusters[k].is_different(self.clusters[k])]
208        return (removed, added, updated)
209
210    def is_different(self, prev):
211        removed, added, updated = self.diff_clusters(prev)
212        return len(removed) != 0 or len(added) != 0 or len(updated) != 0
213
214
215# Deserialized messages from a single checker in a single program state.
216# Basically a list of raw strings.
217class CheckerLines(object):
218    def __init__(self, json_lines):
219        super(CheckerLines, self).__init__()
220        self.lines = json_lines
221
222    def diff_lines(self, prev):
223        lines = difflib.ndiff(prev.lines, self.lines)
224        return [l.strip() for l in lines
225                if l.startswith('+') or l.startswith('-')]
226
227    def is_different(self, prev):
228        return len(self.diff_lines(prev)) > 0
229
230
231# Deserialized messages of all checkers, separated by checker.
232class CheckerMessages(object):
233    def __init__(self, json_m):
234        super(CheckerMessages, self).__init__()
235        self.items = collections.OrderedDict(
236            [(m['checker'], CheckerLines(m['messages'])) for m in json_m])
237
238    def diff_messages(self, prev):
239        removed = [k for k in prev.items if k not in self.items]
240        added = [k for k in self.items if k not in prev.items]
241        updated = [k for k in prev.items if k in self.items
242                   and prev.items[k].is_different(self.items[k])]
243        return (removed, added, updated)
244
245    def is_different(self, prev):
246        removed, added, updated = self.diff_messages(prev)
247        return len(removed) != 0 or len(added) != 0 or len(updated) != 0
248
249
250# A deserialized program state.
251class ProgramState(object):
252    def __init__(self, state_id, json_ps):
253        super(ProgramState, self).__init__()
254        logging.debug('Adding ProgramState ' + str(state_id))
255
256        self.state_id = state_id
257
258        self.store = Store(json_ps['store']) \
259            if json_ps['store'] is not None else None
260
261        self.environment = \
262            GenericEnvironment(json_ps['environment']['items']) \
263            if json_ps['environment'] is not None else None
264
265        self.constraints = GenericMap([
266            (c['symbol'], c['range']) for c in json_ps['constraints']
267        ]) if json_ps['constraints'] is not None else None
268
269        self.dynamic_types = GenericMap([
270                (t['region'], '%s%s' % (t['dyn_type'],
271                                        ' (or a sub-class)'
272                                        if t['sub_classable'] else ''))
273                for t in json_ps['dynamic_types']]) \
274            if json_ps['dynamic_types'] is not None else None
275
276        self.constructing_objects = \
277            GenericEnvironment(json_ps['constructing_objects']) \
278            if json_ps['constructing_objects'] is not None else None
279
280        self.checker_messages = CheckerMessages(json_ps['checker_messages']) \
281            if json_ps['checker_messages'] is not None else None
282
283
284# A deserialized exploded graph node. Has a default constructor because it
285# may be referenced as part of an edge before its contents are deserialized,
286# and in this moment we already need a room for predecessors and successors.
287class ExplodedNode(object):
288    def __init__(self):
289        super(ExplodedNode, self).__init__()
290        self.predecessors = []
291        self.successors = []
292
293    def construct(self, node_id, json_node):
294        logging.debug('Adding ' + node_id)
295        self.node_id = json_node['node_id']
296        self.ptr = json_node['pointer']
297        self.points = [ProgramPoint(p) for p in json_node['program_points']]
298        self.state = ProgramState(json_node['state_id'],
299                                  json_node['program_state']) \
300            if json_node['program_state'] is not None else None
301
302        assert self.node_name() == node_id
303
304    def node_name(self):
305        return 'Node' + self.ptr
306
307
308# A deserialized ExplodedGraph. Constructed by consuming a .dot file
309# line-by-line.
310class ExplodedGraph(object):
311    # Parse .dot files with regular expressions.
312    node_re = re.compile(
313        '^(Node0x[0-9a-f]*) \\[shape=record,.*label="{(.*)\\\\l}"\\];$')
314    edge_re = re.compile(
315        '^(Node0x[0-9a-f]*) -> (Node0x[0-9a-f]*);$')
316
317    def __init__(self):
318        super(ExplodedGraph, self).__init__()
319        self.nodes = collections.defaultdict(ExplodedNode)
320        self.root_id = None
321        self.incomplete_line = ''
322
323    def add_raw_line(self, raw_line):
324        if raw_line.startswith('//'):
325            return
326
327        # Allow line breaks by waiting for ';'. This is not valid in
328        # a .dot file, but it is useful for writing tests.
329        if len(raw_line) > 0 and raw_line[-1] != ';':
330            self.incomplete_line += raw_line
331            return
332        raw_line = self.incomplete_line + raw_line
333        self.incomplete_line = ''
334
335        # Apply regexps one by one to see if it's a node or an edge
336        # and extract contents if necessary.
337        logging.debug('Line: ' + raw_line)
338        result = self.edge_re.match(raw_line)
339        if result is not None:
340            logging.debug('Classified as edge line.')
341            pred = result.group(1)
342            succ = result.group(2)
343            self.nodes[pred].successors.append(succ)
344            self.nodes[succ].predecessors.append(pred)
345            return
346        result = self.node_re.match(raw_line)
347        if result is not None:
348            logging.debug('Classified as node line.')
349            node_id = result.group(1)
350            if len(self.nodes) == 0:
351                self.root_id = node_id
352            # Note: when writing tests you don't need to escape everything,
353            # even though in a valid dot file everything is escaped.
354            node_label = result.group(2).replace('\\l', '') \
355                                        .replace(' ', '') \
356                                        .replace('\\"', '"') \
357                                        .replace('\\{', '{') \
358                                        .replace('\\}', '}') \
359                                        .replace('\\\\', '\\') \
360                                        .replace('\\|', '|') \
361                                        .replace('\\<', '\\\\<') \
362                                        .replace('\\>', '\\\\>') \
363                                        .rstrip(',')
364            logging.debug(node_label)
365            json_node = json.loads(node_label)
366            self.nodes[node_id].construct(node_id, json_node)
367            return
368        logging.debug('Skipping.')
369
370
371# A visitor that dumps the ExplodedGraph into a DOT file with fancy HTML-based
372# syntax highlighing.
373class DotDumpVisitor(object):
374    def __init__(self, do_diffs, dark_mode):
375        super(DotDumpVisitor, self).__init__()
376        self._do_diffs = do_diffs
377        self._dark_mode = dark_mode
378
379    @staticmethod
380    def _dump_raw(s):
381        print(s, end='')
382
383    @staticmethod
384    def _dump(s):
385        print(s.replace('&', '&amp;')
386               .replace('{', '\\{')
387               .replace('}', '\\}')
388               .replace('\\<', '&lt;')
389               .replace('\\>', '&gt;')
390               .replace('\\l', '<br />')
391               .replace('|', '\\|'), end='')
392
393    @staticmethod
394    def _diff_plus_minus(is_added):
395        if is_added is None:
396            return ''
397        if is_added:
398            return '<font color="forestgreen">+</font>'
399        return '<font color="red">-</font>'
400
401    @staticmethod
402    def _short_pretty(s):
403        if s is None:
404            return None
405        if len(s) < 20:
406            return s
407        left = s.find('{')
408        right = s.rfind('}')
409        if left == -1 or right == -1 or left >= right:
410            return s
411        candidate = s[0:left + 1] + ' ... ' + s[right:]
412        if len(candidate) >= len(s):
413            return s
414        return candidate
415
416    def visit_begin_graph(self, graph):
417        self._graph = graph
418        self._dump_raw('digraph "ExplodedGraph" {\n')
419        if self._dark_mode:
420            self._dump_raw('bgcolor="gray10";\n')
421        self._dump_raw('label="";\n')
422
423    def visit_program_point(self, p):
424        if p.kind in ['Edge', 'BlockEntrance', 'BlockExit']:
425            color = 'gold3'
426        elif p.kind in ['PreStmtPurgeDeadSymbols',
427                        'PostStmtPurgeDeadSymbols']:
428            color = 'red'
429        elif p.kind in ['CallEnter', 'CallExitBegin', 'CallExitEnd']:
430            color = 'dodgerblue' if self._dark_mode else 'blue'
431        elif p.kind in ['Statement']:
432            color = 'cyan4'
433        else:
434            color = 'forestgreen'
435
436        if p.kind == 'Statement':
437            # This avoids pretty-printing huge statements such as CompoundStmt.
438            # Such statements show up only at [Pre|Post]StmtPurgeDeadSymbols
439            skip_pretty = 'PurgeDeadSymbols' in p.stmt_point_kind
440            stmt_color = 'cyan3'
441            if p.loc is not None:
442                self._dump('<tr><td align="left" width="0">'
443                           '%s:<b>%s</b>:<b>%s</b>:</td>'
444                           '<td align="left" width="0"><font color="%s">'
445                           '%s</font></td>'
446                           '<td align="left"><font color="%s">%s</font></td>'
447                           '<td>%s</td></tr>'
448                           % (p.loc.filename, p.loc.line,
449                              p.loc.col, color, p.stmt_kind,
450                              stmt_color, p.stmt_point_kind,
451                              self._short_pretty(p.pretty)
452                              if not skip_pretty else ''))
453            else:
454                self._dump('<tr><td align="left" width="0">'
455                           '<i>Invalid Source Location</i>:</td>'
456                           '<td align="left" width="0">'
457                           '<font color="%s">%s</font></td>'
458                           '<td align="left"><font color="%s">%s</font></td>'
459                           '<td>%s</td></tr>'
460                           % (color, p.stmt_kind,
461                              stmt_color, p.stmt_point_kind,
462                              self._short_pretty(p.pretty)
463                              if not skip_pretty else ''))
464        elif p.kind == 'Edge':
465            self._dump('<tr><td width="0"></td>'
466                       '<td align="left" width="0">'
467                       '<font color="%s">%s</font></td><td align="left">'
468                       '[B%d] -\\> [B%d]</td></tr>'
469                       % (color, 'BlockEdge', p.src_id, p.dst_id))
470        elif p.kind == 'BlockEntrance':
471            self._dump('<tr><td width="0"></td>'
472                       '<td align="left" width="0">'
473                       '<font color="%s">%s</font></td>'
474                       '<td align="left">[B%d]</td></tr>'
475                       % (color, p.kind, p.block_id))
476        else:
477            # TODO: Print more stuff for other kinds of points.
478            self._dump('<tr><td width="0"></td>'
479                       '<td align="left" width="0" colspan="2">'
480                       '<font color="%s">%s</font></td></tr>'
481                       % (color, p.kind))
482
483        if p.tag is not None:
484            self._dump('<tr><td width="0"></td>'
485                       '<td colspan="3" align="left">'
486                       '<b>Tag: </b> <font color="crimson">'
487                       '%s</font></td></tr>' % p.tag)
488
489    def visit_environment(self, e, prev_e=None):
490        self._dump('<table border="0">')
491
492        def dump_location_context(lc, is_added=None):
493            self._dump('<tr><td>%s</td>'
494                       '<td align="left"><b>%s</b></td>'
495                       '<td align="left" colspan="2">'
496                       '<font color="gray60">%s </font>'
497                       '%s</td></tr>'
498                       % (self._diff_plus_minus(is_added),
499                          lc.caption, lc.decl,
500                          ('(line %s)' % lc.line) if lc.line is not None
501                          else ''))
502
503        def dump_binding(f, b, is_added=None):
504            self._dump('<tr><td>%s</td>'
505                       '<td align="left"><i>S%s</i></td>'
506                       '%s'
507                       '<td align="left">%s</td>'
508                       '<td align="left">%s</td></tr>'
509                       % (self._diff_plus_minus(is_added),
510                          b.stmt_id,
511                          '<td align="left"><font color="%s"><i>'
512                          '%s</i></font></td>' % (
513                              'lavender' if self._dark_mode else 'darkgreen',
514                              ('(%s)' % b.kind) if b.kind is not None else ' '
515                          ),
516                          self._short_pretty(b.pretty), f.bindings[b]))
517
518        frames_updated = e.diff_frames(prev_e) if prev_e is not None else None
519        if frames_updated:
520            for i in frames_updated:
521                f = e.frames[i]
522                prev_f = prev_e.frames[i]
523                dump_location_context(f.location_context)
524                bindings_removed, bindings_added = f.diff_bindings(prev_f)
525                for b in bindings_removed:
526                    dump_binding(prev_f, b, False)
527                for b in bindings_added:
528                    dump_binding(f, b, True)
529        else:
530            for f in e.frames:
531                dump_location_context(f.location_context)
532                for b in f.bindings:
533                    dump_binding(f, b)
534
535        self._dump('</table>')
536
537    def visit_environment_in_state(self, selector, title, s, prev_s=None):
538        e = getattr(s, selector)
539        prev_e = getattr(prev_s, selector) if prev_s is not None else None
540        if e is None and prev_e is None:
541            return
542
543        self._dump('<hr /><tr><td align="left"><b>%s: </b>' % title)
544        if e is None:
545            self._dump('<i> Nothing!</i>')
546        else:
547            if prev_e is not None:
548                if e.is_different(prev_e):
549                    self._dump('</td></tr><tr><td align="left">')
550                    self.visit_environment(e, prev_e)
551                else:
552                    self._dump('<i> No changes!</i>')
553            else:
554                self._dump('</td></tr><tr><td align="left">')
555                self.visit_environment(e)
556
557        self._dump('</td></tr>')
558
559    def visit_store(self, s, prev_s=None):
560        self._dump('<table border="0">')
561
562        def dump_binding(s, c, b, is_added=None):
563            self._dump('<tr><td>%s</td>'
564                       '<td align="left">%s</td>'
565                       '<td align="left">%s</td>'
566                       '<td align="left">%s</td>'
567                       '<td align="left">%s</td></tr>'
568                       % (self._diff_plus_minus(is_added),
569                          s.clusters[c].base_region, b.offset,
570                          '(<i>Default</i>)' if b.kind == 'Default'
571                          else '',
572                          s.clusters[c].bindings[b]))
573
574        if prev_s is not None:
575            clusters_removed, clusters_added, clusters_updated = \
576                s.diff_clusters(prev_s)
577            for c in clusters_removed:
578                for b in prev_s.clusters[c].bindings:
579                    dump_binding(prev_s, c, b, False)
580            for c in clusters_updated:
581                bindings_removed, bindings_added = \
582                    s.clusters[c].diff_bindings(prev_s.clusters[c])
583                for b in bindings_removed:
584                    dump_binding(prev_s, c, b, False)
585                for b in bindings_added:
586                    dump_binding(s, c, b, True)
587            for c in clusters_added:
588                for b in s.clusters[c].bindings:
589                    dump_binding(s, c, b, True)
590        else:
591            for c in s.clusters:
592                for b in s.clusters[c].bindings:
593                    dump_binding(s, c, b)
594
595        self._dump('</table>')
596
597    def visit_store_in_state(self, s, prev_s=None):
598        st = s.store
599        prev_st = prev_s.store if prev_s is not None else None
600        if st is None and prev_st is None:
601            return
602
603        self._dump('<hr /><tr><td align="left"><b>Store: </b>')
604        if st is None:
605            self._dump('<i> Nothing!</i>')
606        else:
607            if prev_st is not None:
608                if s.store.is_different(prev_st):
609                    self._dump('</td></tr><tr><td align="left">')
610                    self.visit_store(st, prev_st)
611                else:
612                    self._dump('<i> No changes!</i>')
613            else:
614                self._dump('</td></tr><tr><td align="left">')
615                self.visit_store(st)
616        self._dump('</td></tr>')
617
618    def visit_generic_map(self, m, prev_m=None):
619        self._dump('<table border="0">')
620
621        def dump_pair(m, k, is_added=None):
622            self._dump('<tr><td>%s</td>'
623                       '<td align="left">%s</td>'
624                       '<td align="left">%s</td></tr>'
625                       % (self._diff_plus_minus(is_added),
626                          k, m.generic_map[k]))
627
628        if prev_m is not None:
629            removed, added = m.diff(prev_m)
630            for k in removed:
631                dump_pair(prev_m, k, False)
632            for k in added:
633                dump_pair(m, k, True)
634        else:
635            for k in m.generic_map:
636                dump_pair(m, k, None)
637
638        self._dump('</table>')
639
640    def visit_generic_map_in_state(self, selector, title, s, prev_s=None):
641        m = getattr(s, selector)
642        prev_m = getattr(prev_s, selector) if prev_s is not None else None
643        if m is None and prev_m is None:
644            return
645
646        self._dump('<hr />')
647        self._dump('<tr><td align="left">'
648                   '<b>%s: </b>' % title)
649        if m is None:
650            self._dump('<i> Nothing!</i>')
651        else:
652            if prev_m is not None:
653                if m.is_different(prev_m):
654                    self._dump('</td></tr><tr><td align="left">')
655                    self.visit_generic_map(m, prev_m)
656                else:
657                    self._dump('<i> No changes!</i>')
658            else:
659                self._dump('</td></tr><tr><td align="left">')
660                self.visit_generic_map(m)
661
662        self._dump('</td></tr>')
663
664    def visit_checker_messages(self, m, prev_m=None):
665        self._dump('<table border="0">')
666
667        def dump_line(l, is_added=None):
668            self._dump('<tr><td>%s</td>'
669                       '<td align="left">%s</td></tr>'
670                       % (self._diff_plus_minus(is_added), l))
671
672        def dump_chk(chk, is_added=None):
673            dump_line('<i>%s</i>:' % chk, is_added)
674
675        if prev_m is not None:
676            removed, added, updated = m.diff_messages(prev_m)
677            for chk in removed:
678                dump_chk(chk, False)
679                for l in prev_m.items[chk].lines:
680                    dump_line(l, False)
681            for chk in updated:
682                dump_chk(chk)
683                for l in m.items[chk].diff_lines(prev_m.items[chk]):
684                    dump_line(l[1:], l.startswith('+'))
685            for chk in added:
686                dump_chk(chk, True)
687                for l in m.items[chk].lines:
688                    dump_line(l, True)
689        else:
690            for chk in m.items:
691                dump_chk(chk)
692                for l in m.items[chk].lines:
693                    dump_line(l)
694
695        self._dump('</table>')
696
697    def visit_checker_messages_in_state(self, s, prev_s=None):
698        m = s.checker_messages
699        prev_m = prev_s.checker_messages if prev_s is not None else None
700        if m is None and prev_m is None:
701            return
702
703        self._dump('<hr />')
704        self._dump('<tr><td align="left">'
705                   '<b>Checker State: </b>')
706        if m is None:
707            self._dump('<i> Nothing!</i>')
708        else:
709            if prev_m is not None:
710                if m.is_different(prev_m):
711                    self._dump('</td></tr><tr><td align="left">')
712                    self.visit_checker_messages(m, prev_m)
713                else:
714                    self._dump('<i> No changes!</i>')
715            else:
716                self._dump('</td></tr><tr><td align="left">')
717                self.visit_checker_messages(m)
718
719        self._dump('</td></tr>')
720
721    def visit_state(self, s, prev_s):
722        self.visit_store_in_state(s, prev_s)
723        self.visit_environment_in_state('environment', 'Environment',
724                                        s, prev_s)
725        self.visit_generic_map_in_state('constraints', 'Ranges',
726                                        s, prev_s)
727        self.visit_generic_map_in_state('dynamic_types', 'Dynamic Types',
728                                        s, prev_s)
729        self.visit_environment_in_state('constructing_objects',
730                                        'Objects Under Construction',
731                                        s, prev_s)
732        self.visit_checker_messages_in_state(s, prev_s)
733
734    def visit_node(self, node):
735        self._dump('%s [shape=record,'
736                   % (node.node_name()))
737        if self._dark_mode:
738            self._dump('color="white",fontcolor="gray80",')
739        self._dump('label=<<table border="0">')
740
741        self._dump('<tr><td bgcolor="%s"><b>Node %d (%s) - '
742                   'State %s</b></td></tr>'
743                   % ("gray20" if self._dark_mode else "gray",
744                      node.node_id, node.ptr, node.state.state_id
745                      if node.state is not None else 'Unspecified'))
746        self._dump('<tr><td align="left" width="0">')
747        if len(node.points) > 1:
748            self._dump('<b>Program points:</b></td></tr>')
749        else:
750            self._dump('<b>Program point:</b></td></tr>')
751        self._dump('<tr><td align="left" width="0">'
752                   '<table border="0" align="left" width="0">')
753        for p in node.points:
754            self.visit_program_point(p)
755        self._dump('</table></td></tr>')
756
757        if node.state is not None:
758            prev_s = None
759            # Do diffs only when we have a unique predecessor.
760            # Don't do diffs on the leaf nodes because they're
761            # the important ones.
762            if self._do_diffs and len(node.predecessors) == 1 \
763               and len(node.successors) > 0:
764                prev_s = self._graph.nodes[node.predecessors[0]].state
765            self.visit_state(node.state, prev_s)
766        self._dump_raw('</table>>];\n')
767
768    def visit_edge(self, pred, succ):
769        self._dump_raw('%s -> %s%s;\n' % (
770            pred.node_name(), succ.node_name(),
771            ' [color="white"]' if self._dark_mode else ''
772        ))
773
774    def visit_end_of_graph(self):
775        self._dump_raw('}\n')
776
777
778# A class that encapsulates traversal of the ExplodedGraph. Different explorer
779# kinds could potentially traverse specific sub-graphs.
780class Explorer(object):
781    def __init__(self):
782        super(Explorer, self).__init__()
783
784    def explore(self, graph, visitor):
785        visitor.visit_begin_graph(graph)
786        for node in sorted(graph.nodes):
787            logging.debug('Visiting ' + node)
788            visitor.visit_node(graph.nodes[node])
789            for succ in sorted(graph.nodes[node].successors):
790                logging.debug('Visiting edge: %s -> %s ' % (node, succ))
791                visitor.visit_edge(graph.nodes[node], graph.nodes[succ])
792        visitor.visit_end_of_graph()
793
794
795def main():
796    parser = argparse.ArgumentParser()
797    parser.add_argument('filename', type=str)
798    parser.add_argument('-v', '--verbose', action='store_const',
799                        dest='loglevel', const=logging.DEBUG,
800                        default=logging.WARNING,
801                        help='enable info prints')
802    parser.add_argument('-d', '--diff', action='store_const', dest='diff',
803                        const=True, default=False,
804                        help='display differences between states')
805    parser.add_argument('--dark', action='store_const', dest='dark',
806                        const=True, default=False,
807                        help='dark mode')
808    args = parser.parse_args()
809    logging.basicConfig(level=args.loglevel)
810
811    graph = ExplodedGraph()
812    with open(args.filename) as fd:
813        for raw_line in fd:
814            raw_line = raw_line.strip()
815            graph.add_raw_line(raw_line)
816
817    explorer = Explorer()
818    visitor = DotDumpVisitor(args.diff, args.dark)
819    explorer.explore(graph, visitor)
820
821
822if __name__ == '__main__':
823    main()
824