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