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('&', '&') 386 .replace('{', '\\{') 387 .replace('}', '\\}') 388 .replace('\\<', '<') 389 .replace('\\>', '>') 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