| #!/usr/bin/env python | 
 | # | 
 | # ===- exploded-graph-rewriter.py - ExplodedGraph dump tool -----*- python -*--# | 
 | # | 
 | # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | 
 | # See https://llvm.org/LICENSE.txt for license information. | 
 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | 
 | # | 
 | # ===-----------------------------------------------------------------------===# | 
 |  | 
 |  | 
 | from __future__ import print_function | 
 |  | 
 | import argparse | 
 | import collections | 
 | import difflib | 
 | import json | 
 | import logging | 
 | import os | 
 | import re | 
 |  | 
 |  | 
 | # ===-----------------------------------------------------------------------===# | 
 | # These data structures represent a deserialized ExplodedGraph. | 
 | # ===-----------------------------------------------------------------------===# | 
 |  | 
 |  | 
 | # A helper function for finding the difference between two dictionaries. | 
 | def diff_dicts(curr, prev): | 
 |     removed = [k for k in prev if k not in curr or curr[k] != prev[k]] | 
 |     added = [k for k in curr if k not in prev or curr[k] != prev[k]] | 
 |     return (removed, added) | 
 |  | 
 |  | 
 | # Represents any program state trait that is a dictionary of key-value pairs. | 
 | class GenericMap: | 
 |     def __init__(self, items): | 
 |         self.generic_map = collections.OrderedDict(items) | 
 |  | 
 |     def diff(self, prev): | 
 |         return diff_dicts(self.generic_map, prev.generic_map) | 
 |  | 
 |     def is_different(self, prev): | 
 |         removed, added = self.diff(prev) | 
 |         return len(removed) != 0 or len(added) != 0 | 
 |  | 
 |  | 
 | # A deserialized source location. | 
 | class SourceLocation: | 
 |     def __init__(self, json_loc): | 
 |         logging.debug("json: %s" % json_loc) | 
 |         self.line = json_loc["line"] | 
 |         self.col = json_loc["column"] | 
 |         self.filename = ( | 
 |             os.path.basename(json_loc["file"]) if "file" in json_loc else "(main file)" | 
 |         ) | 
 |         self.spelling = ( | 
 |             SourceLocation(json_loc["spelling"]) if "spelling" in json_loc else None | 
 |         ) | 
 |  | 
 |     def is_macro(self): | 
 |         return self.spelling is not None | 
 |  | 
 |  | 
 | # A deserialized program point. | 
 | class ProgramPoint: | 
 |     def __init__(self, json_pp): | 
 |         self.kind = json_pp["kind"] | 
 |         self.tag = json_pp["tag"] | 
 |         self.node_id = json_pp["node_id"] | 
 |         self.is_sink = bool(json_pp["is_sink"]) | 
 |         self.has_report = bool(json_pp["has_report"]) | 
 |         if self.kind == "Edge": | 
 |             self.src_id = json_pp["src_id"] | 
 |             self.dst_id = json_pp["dst_id"] | 
 |         elif self.kind == "Statement": | 
 |             logging.debug(json_pp) | 
 |             self.stmt_kind = json_pp["stmt_kind"] | 
 |             self.cast_kind = json_pp["cast_kind"] if "cast_kind" in json_pp else None | 
 |             self.stmt_point_kind = json_pp["stmt_point_kind"] | 
 |             self.stmt_id = json_pp["stmt_id"] | 
 |             self.pointer = json_pp["pointer"] | 
 |             self.pretty = json_pp["pretty"] | 
 |             self.loc = ( | 
 |                 SourceLocation(json_pp["location"]) | 
 |                 if json_pp["location"] is not None | 
 |                 else None | 
 |             ) | 
 |         elif self.kind == "BlockEntrance": | 
 |             self.block_id = json_pp["block_id"] | 
 |  | 
 |  | 
 | # A single expression acting as a key in a deserialized Environment. | 
 | class EnvironmentBindingKey: | 
 |     def __init__(self, json_ek): | 
 |         # CXXCtorInitializer is not a Stmt! | 
 |         self.stmt_id = ( | 
 |             json_ek["stmt_id"] if "stmt_id" in json_ek else json_ek["init_id"] | 
 |         ) | 
 |         self.pretty = json_ek["pretty"] | 
 |         self.kind = json_ek["kind"] if "kind" in json_ek else None | 
 |  | 
 |     def _key(self): | 
 |         return self.stmt_id | 
 |  | 
 |     def __eq__(self, other): | 
 |         return self._key() == other._key() | 
 |  | 
 |     def __hash__(self): | 
 |         return hash(self._key()) | 
 |  | 
 |  | 
 | # Deserialized description of a location context. | 
 | class LocationContext: | 
 |     def __init__(self, json_frame): | 
 |         self.lctx_id = json_frame["lctx_id"] | 
 |         self.caption = json_frame["location_context"] | 
 |         self.decl = json_frame["calling"] | 
 |         self.loc = ( | 
 |             SourceLocation(json_frame["location"]) | 
 |             if json_frame["location"] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |     def _key(self): | 
 |         return self.lctx_id | 
 |  | 
 |     def __eq__(self, other): | 
 |         return self._key() == other._key() | 
 |  | 
 |     def __hash__(self): | 
 |         return hash(self._key()) | 
 |  | 
 |  | 
 | # A group of deserialized Environment bindings that correspond to a specific | 
 | # location context. | 
 | class EnvironmentFrame: | 
 |     def __init__(self, json_frame): | 
 |         self.location_context = LocationContext(json_frame) | 
 |         self.bindings = collections.OrderedDict( | 
 |             [(EnvironmentBindingKey(b), b["value"]) for b in json_frame["items"]] | 
 |             if json_frame["items"] is not None | 
 |             else [] | 
 |         ) | 
 |  | 
 |     def diff_bindings(self, prev): | 
 |         return diff_dicts(self.bindings, prev.bindings) | 
 |  | 
 |     def is_different(self, prev): | 
 |         removed, added = self.diff_bindings(prev) | 
 |         return len(removed) != 0 or len(added) != 0 | 
 |  | 
 |  | 
 | # A deserialized Environment. This class can also hold other entities that | 
 | # are similar to Environment, such as Objects Under Construction or | 
 | # Indices Of Elements Under Construction. | 
 | class GenericEnvironment: | 
 |     def __init__(self, json_e): | 
 |         self.frames = [EnvironmentFrame(f) for f in json_e] | 
 |  | 
 |     def diff_frames(self, prev): | 
 |         # TODO: It's difficult to display a good diff when frame numbers shift. | 
 |         if len(self.frames) != len(prev.frames): | 
 |             return None | 
 |  | 
 |         updated = [] | 
 |         for i in range(len(self.frames)): | 
 |             f = self.frames[i] | 
 |             prev_f = prev.frames[i] | 
 |             if f.location_context == prev_f.location_context: | 
 |                 if f.is_different(prev_f): | 
 |                     updated.append(i) | 
 |             else: | 
 |                 # We have the whole frame replaced with another frame. | 
 |                 # TODO: Produce a nice diff. | 
 |                 return None | 
 |  | 
 |         # TODO: Add support for added/removed. | 
 |         return updated | 
 |  | 
 |     def is_different(self, prev): | 
 |         updated = self.diff_frames(prev) | 
 |         return updated is None or len(updated) > 0 | 
 |  | 
 |  | 
 | # A single binding key in a deserialized RegionStore cluster. | 
 | class StoreBindingKey: | 
 |     def __init__(self, json_sk): | 
 |         self.kind = json_sk["kind"] | 
 |         self.offset = json_sk["offset"] | 
 |  | 
 |     def _key(self): | 
 |         return (self.kind, self.offset) | 
 |  | 
 |     def __eq__(self, other): | 
 |         return self._key() == other._key() | 
 |  | 
 |     def __hash__(self): | 
 |         return hash(self._key()) | 
 |  | 
 |  | 
 | # A single cluster of the deserialized RegionStore. | 
 | class StoreCluster: | 
 |     def __init__(self, json_sc): | 
 |         self.base_region = json_sc["cluster"] | 
 |         self.bindings = collections.OrderedDict( | 
 |             [(StoreBindingKey(b), b["value"]) for b in json_sc["items"]] | 
 |         ) | 
 |  | 
 |     def diff_bindings(self, prev): | 
 |         return diff_dicts(self.bindings, prev.bindings) | 
 |  | 
 |     def is_different(self, prev): | 
 |         removed, added = self.diff_bindings(prev) | 
 |         return len(removed) != 0 or len(added) != 0 | 
 |  | 
 |  | 
 | # A deserialized RegionStore. | 
 | class Store: | 
 |     def __init__(self, json_s): | 
 |         self.ptr = json_s["pointer"] | 
 |         self.clusters = collections.OrderedDict( | 
 |             [(c["pointer"], StoreCluster(c)) for c in json_s["items"]] | 
 |         ) | 
 |  | 
 |     def diff_clusters(self, prev): | 
 |         removed = [k for k in prev.clusters if k not in self.clusters] | 
 |         added = [k for k in self.clusters if k not in prev.clusters] | 
 |         updated = [ | 
 |             k | 
 |             for k in prev.clusters | 
 |             if k in self.clusters and prev.clusters[k].is_different(self.clusters[k]) | 
 |         ] | 
 |         return (removed, added, updated) | 
 |  | 
 |     def is_different(self, prev): | 
 |         removed, added, updated = self.diff_clusters(prev) | 
 |         return len(removed) != 0 or len(added) != 0 or len(updated) != 0 | 
 |  | 
 |  | 
 | # Deserialized messages from a single checker in a single program state. | 
 | # Basically a list of raw strings. | 
 | class CheckerLines: | 
 |     def __init__(self, json_lines): | 
 |         self.lines = json_lines | 
 |  | 
 |     def diff_lines(self, prev): | 
 |         lines = difflib.ndiff(prev.lines, self.lines) | 
 |         return [l.strip() for l in lines if l.startswith("+") or l.startswith("-")] | 
 |  | 
 |     def is_different(self, prev): | 
 |         return len(self.diff_lines(prev)) > 0 | 
 |  | 
 |  | 
 | # Deserialized messages of all checkers, separated by checker. | 
 | class CheckerMessages: | 
 |     def __init__(self, json_m): | 
 |         self.items = collections.OrderedDict( | 
 |             [(m["checker"], CheckerLines(m["messages"])) for m in json_m] | 
 |         ) | 
 |  | 
 |     def diff_messages(self, prev): | 
 |         removed = [k for k in prev.items if k not in self.items] | 
 |         added = [k for k in self.items if k not in prev.items] | 
 |         updated = [ | 
 |             k | 
 |             for k in prev.items | 
 |             if k in self.items and prev.items[k].is_different(self.items[k]) | 
 |         ] | 
 |         return (removed, added, updated) | 
 |  | 
 |     def is_different(self, prev): | 
 |         removed, added, updated = self.diff_messages(prev) | 
 |         return len(removed) != 0 or len(added) != 0 or len(updated) != 0 | 
 |  | 
 |  | 
 | # A deserialized program state. | 
 | class ProgramState: | 
 |     def __init__(self, state_id, json_ps): | 
 |         logging.debug("Adding ProgramState " + str(state_id)) | 
 |  | 
 |         store_key = "store" | 
 |         env_key = "environment" | 
 |         constraints_key = "constraints" | 
 |         dyn_ty_key = "dynamic_types" | 
 |         ctor_key = "constructing_objects" | 
 |         ind_key = "index_of_element" | 
 |         init_loop_key = "pending_init_loops" | 
 |         dtor_key = "pending_destructors" | 
 |         msg_key = "checker_messages" | 
 |  | 
 |         if json_ps is None: | 
 |             json_ps = { | 
 |                 store_key: None, | 
 |                 env_key: None, | 
 |                 constraints_key: None, | 
 |                 dyn_ty_key: None, | 
 |                 ctor_key: None, | 
 |                 ind_key: None, | 
 |                 init_loop_key: None, | 
 |                 dtor_key: None, | 
 |                 msg_key: None, | 
 |             } | 
 |  | 
 |         self.state_id = state_id | 
 |  | 
 |         self.store = ( | 
 |             Store(json_ps[store_key]) if json_ps[store_key] is not None else None | 
 |         ) | 
 |  | 
 |         self.environment = ( | 
 |             GenericEnvironment(json_ps[env_key]["items"]) | 
 |             if json_ps[env_key] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |         self.constraints = ( | 
 |             GenericMap([(c["symbol"], c["range"]) for c in json_ps[constraints_key]]) | 
 |             if json_ps[constraints_key] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |         self.dynamic_types = ( | 
 |             GenericMap( | 
 |                 [ | 
 |                     ( | 
 |                         t["region"], | 
 |                         "%s%s" | 
 |                         % ( | 
 |                             t["dyn_type"], | 
 |                             " (or a sub-class)" if t["sub_classable"] else "", | 
 |                         ), | 
 |                     ) | 
 |                     for t in json_ps[dyn_ty_key] | 
 |                 ] | 
 |             ) | 
 |             if json_ps[dyn_ty_key] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |         self.checker_messages = ( | 
 |             CheckerMessages(json_ps[msg_key]) if json_ps[msg_key] is not None else None | 
 |         ) | 
 |  | 
 |         # State traits | 
 |         # | 
 |         # For traits we always check if a key exists because if a trait | 
 |         # has no imformation, nothing will be printed in the .dot file | 
 |         # we parse. | 
 |  | 
 |         self.constructing_objects = ( | 
 |             GenericEnvironment(json_ps[ctor_key]) | 
 |             if ctor_key in json_ps and json_ps[ctor_key] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |         self.index_of_element = ( | 
 |             GenericEnvironment(json_ps[ind_key]) | 
 |             if ind_key in json_ps and json_ps[ind_key] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |         self.pending_init_loops = ( | 
 |             GenericEnvironment(json_ps[init_loop_key]) | 
 |             if init_loop_key in json_ps and json_ps[init_loop_key] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |         self.pending_destructors = ( | 
 |             GenericEnvironment(json_ps[dtor_key]) | 
 |             if dtor_key in json_ps and json_ps[dtor_key] is not None | 
 |             else None | 
 |         ) | 
 |  | 
 |  | 
 | # A deserialized exploded graph node. Has a default constructor because it | 
 | # may be referenced as part of an edge before its contents are deserialized, | 
 | # and in this moment we already need a room for predecessors and successors. | 
 | class ExplodedNode: | 
 |     def __init__(self): | 
 |         self.predecessors = [] | 
 |         self.successors = [] | 
 |  | 
 |     def construct(self, node_id, json_node): | 
 |         logging.debug("Adding " + node_id) | 
 |         self.ptr = node_id[4:] | 
 |         self.points = [ProgramPoint(p) for p in json_node["program_points"]] | 
 |         self.node_id = self.points[-1].node_id | 
 |         self.state = ProgramState( | 
 |             json_node["state_id"], | 
 |             json_node["program_state"] | 
 |             if json_node["program_state"] is not None | 
 |             else None, | 
 |         ) | 
 |  | 
 |         assert self.node_name() == node_id | 
 |  | 
 |     def node_name(self): | 
 |         return "Node" + self.ptr | 
 |  | 
 |  | 
 | # A deserialized ExplodedGraph. Constructed by consuming a .dot file | 
 | # line-by-line. | 
 | class ExplodedGraph: | 
 |     # Parse .dot files with regular expressions. | 
 |     node_re = re.compile( | 
 |         '^(Node0x[0-9a-f]*) \\[shape=record,.*label="{(.*)\\\\l}"\\];$' | 
 |     ) | 
 |     edge_re = re.compile("^(Node0x[0-9a-f]*) -> (Node0x[0-9a-f]*);$") | 
 |  | 
 |     def __init__(self): | 
 |         self.nodes = collections.defaultdict(ExplodedNode) | 
 |         self.root_id = None | 
 |         self.incomplete_line = "" | 
 |  | 
 |     def add_raw_line(self, raw_line): | 
 |         if raw_line.startswith("//"): | 
 |             return | 
 |  | 
 |         # Allow line breaks by waiting for ';'. This is not valid in | 
 |         # a .dot file, but it is useful for writing tests. | 
 |         if len(raw_line) > 0 and raw_line[-1] != ";": | 
 |             self.incomplete_line += raw_line | 
 |             return | 
 |         raw_line = self.incomplete_line + raw_line | 
 |         self.incomplete_line = "" | 
 |  | 
 |         # Apply regexps one by one to see if it's a node or an edge | 
 |         # and extract contents if necessary. | 
 |         logging.debug("Line: " + raw_line) | 
 |         result = self.edge_re.match(raw_line) | 
 |         if result is not None: | 
 |             logging.debug("Classified as edge line.") | 
 |             pred = result.group(1) | 
 |             succ = result.group(2) | 
 |             self.nodes[pred].successors.append(succ) | 
 |             self.nodes[succ].predecessors.append(pred) | 
 |             return | 
 |         result = self.node_re.match(raw_line) | 
 |         if result is not None: | 
 |             logging.debug("Classified as node line.") | 
 |             node_id = result.group(1) | 
 |             if len(self.nodes) == 0: | 
 |                 self.root_id = node_id | 
 |             # Note: when writing tests you don't need to escape everything, | 
 |             # even though in a valid dot file everything is escaped. | 
 |             node_label = ( | 
 |                 result.group(2) | 
 |                 .replace(" ", "") | 
 |                 .replace('\\"', '"') | 
 |                 .replace("\\{", "{") | 
 |                 .replace("\\}", "}") | 
 |                 .replace("\\\\", "\\") | 
 |                 .replace("\\|", "|") | 
 |                 .replace("\\<", "\\\\<") | 
 |                 .replace("\\>", "\\\\>") | 
 |                 .rstrip(",") | 
 |             ) | 
 |             # Handle `\l` separately because a string literal can be in code | 
 |             # like "string\\literal" with the `\l` inside. | 
 |             # Also on Windows macros __FILE__ produces specific delimiters `\` | 
 |             # and a directory or file may starts with the letter `l`. | 
 |             # Find all `\l` (like `,\l`, `}\l`, `[\l`) except `\\l`, | 
 |             # because the literal as a rule contains multiple `\` before `\l`. | 
 |             node_label = re.sub(r"(?<!\\)\\l", "", node_label) | 
 |             logging.debug(node_label) | 
 |             json_node = json.loads(node_label) | 
 |             self.nodes[node_id].construct(node_id, json_node) | 
 |             return | 
 |         logging.debug("Skipping.") | 
 |  | 
 |  | 
 | # ===-----------------------------------------------------------------------===# | 
 | # Visitors traverse a deserialized ExplodedGraph and do different things | 
 | # with every node and edge. | 
 | # ===-----------------------------------------------------------------------===# | 
 |  | 
 |  | 
 | # A visitor that dumps the ExplodedGraph into a DOT file with fancy HTML-based | 
 | # syntax highlighing. | 
 | class DotDumpVisitor: | 
 |     def __init__(self, do_diffs, dark_mode, gray_mode, topo_mode, dump_dot_only): | 
 |         self._do_diffs = do_diffs | 
 |         self._dark_mode = dark_mode | 
 |         self._gray_mode = gray_mode | 
 |         self._topo_mode = topo_mode | 
 |         self._dump_dot_only = dump_dot_only | 
 |         self._output = [] | 
 |  | 
 |     def _dump_raw(self, s): | 
 |         if self._dump_dot_only: | 
 |             print(s, end="") | 
 |         else: | 
 |             self._output.append(s) | 
 |  | 
 |     def output(self): | 
 |         assert not self._dump_dot_only | 
 |         return "".join(self._output) | 
 |  | 
 |     def _dump(self, s): | 
 |         s = ( | 
 |             s.replace("&", "&") | 
 |             .replace("{", "\\{") | 
 |             .replace("}", "\\}") | 
 |             .replace("\\<", "<") | 
 |             .replace("\\>", ">") | 
 |             .replace("|", "\\|") | 
 |         ) | 
 |         s = re.sub(r"(?<!\\)\\l", "<br />", s) | 
 |         if self._gray_mode: | 
 |             s = re.sub(r'<font color="[a-z0-9]*">', "", s) | 
 |             s = re.sub(r"</font>", "", s) | 
 |         self._dump_raw(s) | 
 |  | 
 |     @staticmethod | 
 |     def _diff_plus_minus(is_added): | 
 |         if is_added is None: | 
 |             return "" | 
 |         if is_added: | 
 |             return '<font color="forestgreen">+</font>' | 
 |         return '<font color="red">-</font>' | 
 |  | 
 |     @staticmethod | 
 |     def _short_pretty(s): | 
 |         if s is None: | 
 |             return None | 
 |         if len(s) < 20: | 
 |             return s | 
 |         left = s.find("{") | 
 |         right = s.rfind("}") | 
 |         if left == -1 or right == -1 or left >= right: | 
 |             return s | 
 |         candidate = s[0 : left + 1] + " ... " + s[right:] | 
 |         if len(candidate) >= len(s): | 
 |             return s | 
 |         return candidate | 
 |  | 
 |     @staticmethod | 
 |     def _make_sloc(loc): | 
 |         if loc is None: | 
 |             return "<i>Invalid Source Location</i>" | 
 |  | 
 |         def make_plain_loc(loc): | 
 |             return "%s:<b>%s</b>:<b>%s</b>" % (loc.filename, loc.line, loc.col) | 
 |  | 
 |         if loc.is_macro(): | 
 |             return '%s <font color="royalblue1">' "(<i>spelling at </i> %s)</font>" % ( | 
 |                 make_plain_loc(loc), | 
 |                 make_plain_loc(loc.spelling), | 
 |             ) | 
 |  | 
 |         return make_plain_loc(loc) | 
 |  | 
 |     def visit_begin_graph(self, graph): | 
 |         self._graph = graph | 
 |         self._dump_raw('digraph "ExplodedGraph" {\n') | 
 |         if self._dark_mode: | 
 |             self._dump_raw('bgcolor="gray10";\n') | 
 |         self._dump_raw('label="";\n') | 
 |  | 
 |     def visit_program_point(self, p): | 
 |         if p.kind in ["Edge", "BlockEntrance", "BlockExit"]: | 
 |             color = "gold3" | 
 |         elif p.kind in ["PreStmtPurgeDeadSymbols", "PostStmtPurgeDeadSymbols"]: | 
 |             color = "red" | 
 |         elif p.kind in ["CallEnter", "CallExitBegin", "CallExitEnd"]: | 
 |             color = "dodgerblue" if self._dark_mode else "blue" | 
 |         elif p.kind in ["Statement"]: | 
 |             color = "cyan4" | 
 |         else: | 
 |             color = "forestgreen" | 
 |  | 
 |         self._dump('<tr><td align="left">%s.</td>' % p.node_id) | 
 |  | 
 |         if p.kind == "Statement": | 
 |             # This avoids pretty-printing huge statements such as CompoundStmt. | 
 |             # Such statements show up only at [Pre|Post]StmtPurgeDeadSymbols | 
 |             skip_pretty = "PurgeDeadSymbols" in p.stmt_point_kind | 
 |             stmt_color = "cyan3" | 
 |             self._dump( | 
 |                 '<td align="left" width="0">%s:</td>' | 
 |                 '<td align="left" width="0"><font color="%s">' | 
 |                 "%s</font> </td>" | 
 |                 '<td align="left"><i>S%s</i></td>' | 
 |                 '<td align="left"><font color="%s">%s</font></td>' | 
 |                 '<td align="left">%s</td></tr>' | 
 |                 % ( | 
 |                     self._make_sloc(p.loc), | 
 |                     color, | 
 |                     "%s (%s)" % (p.stmt_kind, p.cast_kind) | 
 |                     if p.cast_kind is not None | 
 |                     else p.stmt_kind, | 
 |                     p.stmt_id, | 
 |                     stmt_color, | 
 |                     p.stmt_point_kind, | 
 |                     self._short_pretty(p.pretty) if not skip_pretty else "", | 
 |                 ) | 
 |             ) | 
 |         elif p.kind == "Edge": | 
 |             self._dump( | 
 |                 '<td width="0"></td>' | 
 |                 '<td align="left" width="0">' | 
 |                 '<font color="%s">%s</font></td><td align="left">' | 
 |                 "[B%d] -\\> [B%d]</td></tr>" % (color, "BlockEdge", p.src_id, p.dst_id) | 
 |             ) | 
 |         elif p.kind == "BlockEntrance": | 
 |             self._dump( | 
 |                 '<td width="0"></td>' | 
 |                 '<td align="left" width="0">' | 
 |                 '<font color="%s">%s</font></td>' | 
 |                 '<td align="left">[B%d]</td></tr>' % (color, p.kind, p.block_id) | 
 |             ) | 
 |         else: | 
 |             # TODO: Print more stuff for other kinds of points. | 
 |             self._dump( | 
 |                 '<td width="0"></td>' | 
 |                 '<td align="left" width="0" colspan="2">' | 
 |                 '<font color="%s">%s</font></td></tr>' % (color, p.kind) | 
 |             ) | 
 |  | 
 |         if p.tag is not None: | 
 |             self._dump( | 
 |                 '<tr><td width="0"></td><td width="0"></td>' | 
 |                 '<td colspan="3" align="left">' | 
 |                 '<b>Tag: </b> <font color="crimson">' | 
 |                 "%s</font></td></tr>" % p.tag | 
 |             ) | 
 |  | 
 |         if p.has_report: | 
 |             self._dump( | 
 |                 '<tr><td width="0"></td><td width="0"></td>' | 
 |                 '<td colspan="3" align="left">' | 
 |                 '<font color="red"><b>Bug Report Attached' | 
 |                 "</b></font></td></tr>" | 
 |             ) | 
 |         if p.is_sink: | 
 |             self._dump( | 
 |                 '<tr><td width="0"></td><td width="0"></td>' | 
 |                 '<td colspan="3" align="left">' | 
 |                 '<font color="cornflowerblue"><b>Sink Node' | 
 |                 "</b></font></td></tr>" | 
 |             ) | 
 |  | 
 |     def visit_environment(self, e, prev_e=None): | 
 |         self._dump('<table border="0">') | 
 |  | 
 |         def dump_location_context(lc, is_added=None): | 
 |             self._dump( | 
 |                 "<tr><td>%s</td>" | 
 |                 '<td align="left"><b>%s</b></td>' | 
 |                 '<td align="left" colspan="2">' | 
 |                 '<font color="gray60">%s </font>' | 
 |                 "%s</td></tr>" | 
 |                 % ( | 
 |                     self._diff_plus_minus(is_added), | 
 |                     lc.caption, | 
 |                     lc.decl, | 
 |                     ("(%s)" % self._make_sloc(lc.loc)) if lc.loc is not None else "", | 
 |                 ) | 
 |             ) | 
 |  | 
 |         def dump_binding(f, b, is_added=None): | 
 |             self._dump( | 
 |                 "<tr><td>%s</td>" | 
 |                 '<td align="left"><i>S%s</i></td>' | 
 |                 "%s" | 
 |                 '<td align="left">%s</td>' | 
 |                 '<td align="left">%s</td></tr>' | 
 |                 % ( | 
 |                     self._diff_plus_minus(is_added), | 
 |                     b.stmt_id, | 
 |                     '<td align="left"><font color="%s"><i>' | 
 |                     "%s</i></font></td>" | 
 |                     % ( | 
 |                         "lavender" if self._dark_mode else "darkgreen", | 
 |                         ("(%s)" % b.kind) if b.kind is not None else " ", | 
 |                     ), | 
 |                     self._short_pretty(b.pretty), | 
 |                     f.bindings[b], | 
 |                 ) | 
 |             ) | 
 |  | 
 |         frames_updated = e.diff_frames(prev_e) if prev_e is not None else None | 
 |         if frames_updated: | 
 |             for i in frames_updated: | 
 |                 f = e.frames[i] | 
 |                 prev_f = prev_e.frames[i] | 
 |                 dump_location_context(f.location_context) | 
 |                 bindings_removed, bindings_added = f.diff_bindings(prev_f) | 
 |                 for b in bindings_removed: | 
 |                     dump_binding(prev_f, b, False) | 
 |                 for b in bindings_added: | 
 |                     dump_binding(f, b, True) | 
 |         else: | 
 |             for f in e.frames: | 
 |                 dump_location_context(f.location_context) | 
 |                 for b in f.bindings: | 
 |                     dump_binding(f, b) | 
 |  | 
 |         self._dump("</table>") | 
 |  | 
 |     def visit_environment_in_state(self, selector, title, s, prev_s=None): | 
 |         e = getattr(s, selector) | 
 |         prev_e = getattr(prev_s, selector) if prev_s is not None else None | 
 |         if e is None and prev_e is None: | 
 |             return | 
 |  | 
 |         self._dump('<hr /><tr><td align="left"><b>%s: </b>' % title) | 
 |         if e is None: | 
 |             self._dump("<i> Nothing!</i>") | 
 |         else: | 
 |             if prev_e is not None: | 
 |                 if e.is_different(prev_e): | 
 |                     self._dump('</td></tr><tr><td align="left">') | 
 |                     self.visit_environment(e, prev_e) | 
 |                 else: | 
 |                     self._dump("<i> No changes!</i>") | 
 |             else: | 
 |                 self._dump('</td></tr><tr><td align="left">') | 
 |                 self.visit_environment(e) | 
 |  | 
 |         self._dump("</td></tr>") | 
 |  | 
 |     def visit_store(self, s, prev_s=None): | 
 |         self._dump('<table border="0">') | 
 |  | 
 |         def dump_binding(s, c, b, is_added=None): | 
 |             self._dump( | 
 |                 "<tr><td>%s</td>" | 
 |                 '<td align="left">%s</td>' | 
 |                 '<td align="left">%s</td>' | 
 |                 '<td align="left">%s</td>' | 
 |                 '<td align="left">%s</td></tr>' | 
 |                 % ( | 
 |                     self._diff_plus_minus(is_added), | 
 |                     s.clusters[c].base_region, | 
 |                     b.offset, | 
 |                     "(<i>Default</i>)" if b.kind == "Default" else "", | 
 |                     s.clusters[c].bindings[b], | 
 |                 ) | 
 |             ) | 
 |  | 
 |         if prev_s is not None: | 
 |             clusters_removed, clusters_added, clusters_updated = s.diff_clusters(prev_s) | 
 |             for c in clusters_removed: | 
 |                 for b in prev_s.clusters[c].bindings: | 
 |                     dump_binding(prev_s, c, b, False) | 
 |             for c in clusters_updated: | 
 |                 bindings_removed, bindings_added = s.clusters[c].diff_bindings( | 
 |                     prev_s.clusters[c] | 
 |                 ) | 
 |                 for b in bindings_removed: | 
 |                     dump_binding(prev_s, c, b, False) | 
 |                 for b in bindings_added: | 
 |                     dump_binding(s, c, b, True) | 
 |             for c in clusters_added: | 
 |                 for b in s.clusters[c].bindings: | 
 |                     dump_binding(s, c, b, True) | 
 |         else: | 
 |             for c in s.clusters: | 
 |                 for b in s.clusters[c].bindings: | 
 |                     dump_binding(s, c, b) | 
 |  | 
 |         self._dump("</table>") | 
 |  | 
 |     def visit_store_in_state(self, s, prev_s=None): | 
 |         st = s.store | 
 |         prev_st = prev_s.store if prev_s is not None else None | 
 |         if st is None and prev_st is None: | 
 |             return | 
 |  | 
 |         self._dump('<hr /><tr><td align="left"><b>Store: </b>') | 
 |         if st is None: | 
 |             self._dump("<i> Nothing!</i>") | 
 |         else: | 
 |             if self._dark_mode: | 
 |                 self._dump(' <font color="gray30">(%s)</font>' % st.ptr) | 
 |             else: | 
 |                 self._dump(' <font color="gray">(%s)</font>' % st.ptr) | 
 |             if prev_st is not None: | 
 |                 if s.store.is_different(prev_st): | 
 |                     self._dump('</td></tr><tr><td align="left">') | 
 |                     self.visit_store(st, prev_st) | 
 |                 else: | 
 |                     self._dump("<i> No changes!</i>") | 
 |             else: | 
 |                 self._dump('</td></tr><tr><td align="left">') | 
 |                 self.visit_store(st) | 
 |         self._dump("</td></tr>") | 
 |  | 
 |     def visit_generic_map(self, m, prev_m=None): | 
 |         self._dump('<table border="0">') | 
 |  | 
 |         def dump_pair(m, k, is_added=None): | 
 |             self._dump( | 
 |                 "<tr><td>%s</td>" | 
 |                 '<td align="left">%s</td>' | 
 |                 '<td align="left">%s</td></tr>' | 
 |                 % (self._diff_plus_minus(is_added), k, m.generic_map[k]) | 
 |             ) | 
 |  | 
 |         if prev_m is not None: | 
 |             removed, added = m.diff(prev_m) | 
 |             for k in removed: | 
 |                 dump_pair(prev_m, k, False) | 
 |             for k in added: | 
 |                 dump_pair(m, k, True) | 
 |         else: | 
 |             for k in m.generic_map: | 
 |                 dump_pair(m, k, None) | 
 |  | 
 |         self._dump("</table>") | 
 |  | 
 |     def visit_generic_map_in_state(self, selector, title, s, prev_s=None): | 
 |         m = getattr(s, selector) | 
 |         prev_m = getattr(prev_s, selector) if prev_s is not None else None | 
 |         if m is None and prev_m is None: | 
 |             return | 
 |  | 
 |         self._dump("<hr />") | 
 |         self._dump('<tr><td align="left">' "<b>%s: </b>" % title) | 
 |         if m is None: | 
 |             self._dump("<i> Nothing!</i>") | 
 |         else: | 
 |             if prev_m is not None: | 
 |                 if m.is_different(prev_m): | 
 |                     self._dump('</td></tr><tr><td align="left">') | 
 |                     self.visit_generic_map(m, prev_m) | 
 |                 else: | 
 |                     self._dump("<i> No changes!</i>") | 
 |             else: | 
 |                 self._dump('</td></tr><tr><td align="left">') | 
 |                 self.visit_generic_map(m) | 
 |  | 
 |         self._dump("</td></tr>") | 
 |  | 
 |     def visit_checker_messages(self, m, prev_m=None): | 
 |         self._dump('<table border="0">') | 
 |  | 
 |         def dump_line(l, is_added=None): | 
 |             self._dump( | 
 |                 "<tr><td>%s</td>" | 
 |                 '<td align="left">%s</td></tr>' % (self._diff_plus_minus(is_added), l) | 
 |             ) | 
 |  | 
 |         def dump_chk(chk, is_added=None): | 
 |             dump_line("<i>%s</i>:" % chk, is_added) | 
 |  | 
 |         if prev_m is not None: | 
 |             removed, added, updated = m.diff_messages(prev_m) | 
 |             for chk in removed: | 
 |                 dump_chk(chk, False) | 
 |                 for l in prev_m.items[chk].lines: | 
 |                     dump_line(l, False) | 
 |             for chk in updated: | 
 |                 dump_chk(chk) | 
 |                 for l in m.items[chk].diff_lines(prev_m.items[chk]): | 
 |                     dump_line(l[1:], l.startswith("+")) | 
 |             for chk in added: | 
 |                 dump_chk(chk, True) | 
 |                 for l in m.items[chk].lines: | 
 |                     dump_line(l, True) | 
 |         else: | 
 |             for chk in m.items: | 
 |                 dump_chk(chk) | 
 |                 for l in m.items[chk].lines: | 
 |                     dump_line(l) | 
 |  | 
 |         self._dump("</table>") | 
 |  | 
 |     def visit_checker_messages_in_state(self, s, prev_s=None): | 
 |         m = s.checker_messages | 
 |         prev_m = prev_s.checker_messages if prev_s is not None else None | 
 |         if m is None and prev_m is None: | 
 |             return | 
 |  | 
 |         self._dump("<hr />") | 
 |         self._dump('<tr><td align="left">' "<b>Checker State: </b>") | 
 |         if m is None: | 
 |             self._dump("<i> Nothing!</i>") | 
 |         else: | 
 |             if prev_m is not None: | 
 |                 if m.is_different(prev_m): | 
 |                     self._dump('</td></tr><tr><td align="left">') | 
 |                     self.visit_checker_messages(m, prev_m) | 
 |                 else: | 
 |                     self._dump("<i> No changes!</i>") | 
 |             else: | 
 |                 self._dump('</td></tr><tr><td align="left">') | 
 |                 self.visit_checker_messages(m) | 
 |  | 
 |         self._dump("</td></tr>") | 
 |  | 
 |     def visit_state(self, s, prev_s): | 
 |         self.visit_store_in_state(s, prev_s) | 
 |         self.visit_environment_in_state("environment", "Expressions", s, prev_s) | 
 |         self.visit_generic_map_in_state("constraints", "Ranges", s, prev_s) | 
 |         self.visit_generic_map_in_state("dynamic_types", "Dynamic Types", s, prev_s) | 
 |         self.visit_environment_in_state( | 
 |             "constructing_objects", "Objects Under Construction", s, prev_s | 
 |         ) | 
 |         self.visit_environment_in_state( | 
 |             "index_of_element", "Indices Of Elements Under Construction", s, prev_s | 
 |         ) | 
 |         self.visit_environment_in_state( | 
 |             "pending_init_loops", "Pending Array Init Loop Expressions", s, prev_s | 
 |         ) | 
 |         self.visit_environment_in_state( | 
 |             "pending_destructors", "Indices of Elements Under Destruction", s, prev_s | 
 |         ) | 
 |         self.visit_checker_messages_in_state(s, prev_s) | 
 |  | 
 |     def visit_node(self, node): | 
 |         self._dump("%s [shape=record," % (node.node_name())) | 
 |         if self._dark_mode: | 
 |             self._dump('color="white",fontcolor="gray80",') | 
 |         self._dump('label=<<table border="0">') | 
 |  | 
 |         self._dump( | 
 |             '<tr><td bgcolor="%s"><b>State %s</b></td></tr>' | 
 |             % ( | 
 |                 "gray20" if self._dark_mode else "gray70", | 
 |                 node.state.state_id if node.state is not None else "Unspecified", | 
 |             ) | 
 |         ) | 
 |         if not self._topo_mode: | 
 |             self._dump('<tr><td align="left" width="0">') | 
 |             if len(node.points) > 1: | 
 |                 self._dump("<b>Program points:</b></td></tr>") | 
 |             else: | 
 |                 self._dump("<b>Program point:</b></td></tr>") | 
 |         self._dump( | 
 |             '<tr><td align="left" width="0">' | 
 |             '<table border="0" align="left" width="0">' | 
 |         ) | 
 |         for p in node.points: | 
 |             self.visit_program_point(p) | 
 |         self._dump("</table></td></tr>") | 
 |  | 
 |         if node.state is not None and not self._topo_mode: | 
 |             prev_s = None | 
 |             # Do diffs only when we have a unique predecessor. | 
 |             # Don't do diffs on the leaf nodes because they're | 
 |             # the important ones. | 
 |             if ( | 
 |                 self._do_diffs | 
 |                 and len(node.predecessors) == 1 | 
 |                 and len(node.successors) > 0 | 
 |             ): | 
 |                 prev_s = self._graph.nodes[node.predecessors[0]].state | 
 |             self.visit_state(node.state, prev_s) | 
 |         self._dump_raw("</table>>];\n") | 
 |  | 
 |     def visit_edge(self, pred, succ): | 
 |         self._dump_raw( | 
 |             "%s -> %s%s;\n" | 
 |             % ( | 
 |                 pred.node_name(), | 
 |                 succ.node_name(), | 
 |                 ' [color="white"]' if self._dark_mode else "", | 
 |             ) | 
 |         ) | 
 |  | 
 |     def visit_end_of_graph(self): | 
 |         self._dump_raw("}\n") | 
 |  | 
 |         if not self._dump_dot_only: | 
 |             import sys | 
 |             import tempfile | 
 |  | 
 |             def write_temp_file(suffix, prefix, data): | 
 |                 fd, filename = tempfile.mkstemp(suffix, prefix, ".", True) | 
 |                 print('Writing "%s"...' % filename) | 
 |                 with os.fdopen(fd, "w") as fp: | 
 |                     fp.write(data) | 
 |                 print("Done! Please remember to remove the file.") | 
 |                 return filename | 
 |  | 
 |             try: | 
 |                 import graphviz | 
 |             except ImportError: | 
 |                 # The fallback behavior if graphviz is not installed! | 
 |                 print("Python graphviz not found. Please invoke") | 
 |                 print("  $ pip install graphviz") | 
 |                 print("in order to enable automatic conversion to HTML.") | 
 |                 print() | 
 |                 print("You may also convert DOT to SVG manually via") | 
 |                 print("  $ dot -Tsvg input.dot -o output.svg") | 
 |                 print() | 
 |                 write_temp_file(".dot", "egraph-", self.output()) | 
 |                 return | 
 |  | 
 |             svg = graphviz.pipe("dot", "svg", self.output().encode()).decode() | 
 |  | 
 |             filename = write_temp_file( | 
 |                 ".html", | 
 |                 "egraph-", | 
 |                 '<html><body bgcolor="%s">%s</body></html>' | 
 |                 % ("#1a1a1a" if self._dark_mode else "white", svg), | 
 |             ) | 
 |             if sys.platform == "win32": | 
 |                 os.startfile(filename) | 
 |             elif sys.platform == "darwin": | 
 |                 os.system('open "%s"' % filename) | 
 |             else: | 
 |                 os.system('xdg-open "%s"' % filename) | 
 |  | 
 |  | 
 | # ===-----------------------------------------------------------------------===# | 
 | # Explorers know how to traverse the ExplodedGraph in a certain order. | 
 | # They would invoke a Visitor on every node or edge they encounter. | 
 | # ===-----------------------------------------------------------------------===# | 
 |  | 
 |  | 
 | # BasicExplorer explores the whole graph in no particular order. | 
 | class BasicExplorer: | 
 |     def explore(self, graph, visitor): | 
 |         visitor.visit_begin_graph(graph) | 
 |         for node in sorted(graph.nodes): | 
 |             logging.debug("Visiting " + node) | 
 |             visitor.visit_node(graph.nodes[node]) | 
 |             for succ in sorted(graph.nodes[node].successors): | 
 |                 logging.debug("Visiting edge: %s -> %s " % (node, succ)) | 
 |                 visitor.visit_edge(graph.nodes[node], graph.nodes[succ]) | 
 |         visitor.visit_end_of_graph() | 
 |  | 
 |  | 
 | # ===-----------------------------------------------------------------------===# | 
 | # Trimmers cut out parts of the ExplodedGraph so that to focus on other parts. | 
 | # Trimmers can be combined together by applying them sequentially. | 
 | # ===-----------------------------------------------------------------------===# | 
 |  | 
 |  | 
 | # SinglePathTrimmer keeps only a single path - the leftmost path from the root. | 
 | # Useful when the trimmed graph is still too large. | 
 | class SinglePathTrimmer: | 
 |     def trim(self, graph): | 
 |         visited_nodes = set() | 
 |         node_id = graph.root_id | 
 |         while True: | 
 |             visited_nodes.add(node_id) | 
 |             node = graph.nodes[node_id] | 
 |             if len(node.successors) > 0: | 
 |                 succ_id = node.successors[0] | 
 |                 succ = graph.nodes[succ_id] | 
 |                 node.successors = [succ_id] | 
 |                 succ.predecessors = [node_id] | 
 |                 if succ_id in visited_nodes: | 
 |                     break | 
 |                 node_id = succ_id | 
 |             else: | 
 |                 break | 
 |         graph.nodes = {node_id: graph.nodes[node_id] for node_id in visited_nodes} | 
 |  | 
 |  | 
 | # TargetedTrimmer keeps paths that lead to specific nodes and discards all | 
 | # other paths. Useful when you cannot use -trim-egraph (e.g. when debugging | 
 | # a crash). | 
 | class TargetedTrimmer: | 
 |     def __init__(self, target_nodes): | 
 |         self._target_nodes = target_nodes | 
 |  | 
 |     @staticmethod | 
 |     def parse_target_node(node, graph): | 
 |         if node.startswith("0x"): | 
 |             ret = "Node" + node | 
 |             assert ret in graph.nodes | 
 |             return ret | 
 |         else: | 
 |             for other_id in graph.nodes: | 
 |                 other = graph.nodes[other_id] | 
 |                 if other.node_id == int(node): | 
 |                     return other_id | 
 |  | 
 |     @staticmethod | 
 |     def parse_target_nodes(target_nodes, graph): | 
 |         return [ | 
 |             TargetedTrimmer.parse_target_node(node, graph) | 
 |             for node in target_nodes.split(",") | 
 |         ] | 
 |  | 
 |     def trim(self, graph): | 
 |         queue = self._target_nodes | 
 |         visited_nodes = set() | 
 |  | 
 |         while len(queue) > 0: | 
 |             node_id = queue.pop() | 
 |             visited_nodes.add(node_id) | 
 |             node = graph.nodes[node_id] | 
 |             for pred_id in node.predecessors: | 
 |                 if pred_id not in visited_nodes: | 
 |                     queue.append(pred_id) | 
 |         graph.nodes = {node_id: graph.nodes[node_id] for node_id in visited_nodes} | 
 |         for node_id in graph.nodes: | 
 |             node = graph.nodes[node_id] | 
 |             node.successors = [ | 
 |                 succ_id for succ_id in node.successors if succ_id in visited_nodes | 
 |             ] | 
 |             node.predecessors = [ | 
 |                 succ_id for succ_id in node.predecessors if succ_id in visited_nodes | 
 |             ] | 
 |  | 
 |  | 
 | # ===-----------------------------------------------------------------------===# | 
 | # The entry point to the script. | 
 | # ===-----------------------------------------------------------------------===# | 
 |  | 
 |  | 
 | def main(): | 
 |     parser = argparse.ArgumentParser( | 
 |         description="Display and manipulate Exploded Graph dumps." | 
 |     ) | 
 |     parser.add_argument( | 
 |         "filename", type=str, help="the .dot file produced by the Static Analyzer" | 
 |     ) | 
 |     parser.add_argument( | 
 |         "-v", | 
 |         "--verbose", | 
 |         action="store_const", | 
 |         dest="loglevel", | 
 |         const=logging.DEBUG, | 
 |         default=logging.WARNING, | 
 |         help="enable info prints", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "-d", | 
 |         "--diff", | 
 |         action="store_const", | 
 |         dest="diff", | 
 |         const=True, | 
 |         default=False, | 
 |         help="display differences between states", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "-t", | 
 |         "--topology", | 
 |         action="store_const", | 
 |         dest="topology", | 
 |         const=True, | 
 |         default=False, | 
 |         help="only display program points, omit states", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "-s", | 
 |         "--single-path", | 
 |         action="store_const", | 
 |         dest="single_path", | 
 |         const=True, | 
 |         default=False, | 
 |         help="only display the leftmost path in the graph " | 
 |         "(useful for trimmed graphs that still " | 
 |         "branch too much)", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--to", | 
 |         type=str, | 
 |         default=None, | 
 |         help="only display execution paths from the root " | 
 |         "to the given comma-separated list of nodes " | 
 |         "identified by a pointer or a stable ID; " | 
 |         "compatible with --single-path", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--dark", | 
 |         action="store_const", | 
 |         dest="dark", | 
 |         const=True, | 
 |         default=False, | 
 |         help="dark mode", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--gray", | 
 |         action="store_const", | 
 |         dest="gray", | 
 |         const=True, | 
 |         default=False, | 
 |         help="black-and-white mode", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--dump-dot-only", | 
 |         action="store_const", | 
 |         dest="dump_dot_only", | 
 |         const=True, | 
 |         default=False, | 
 |         help="instead of writing an HTML file and immediately " | 
 |         "displaying it, dump the rewritten dot file " | 
 |         "to stdout", | 
 |     ) | 
 |     args = parser.parse_args() | 
 |     logging.basicConfig(level=args.loglevel) | 
 |  | 
 |     graph = ExplodedGraph() | 
 |     with open(args.filename) as fd: | 
 |         for raw_line in fd: | 
 |             raw_line = raw_line.strip() | 
 |             graph.add_raw_line(raw_line) | 
 |  | 
 |     trimmers = [] | 
 |     if args.to is not None: | 
 |         trimmers.append( | 
 |             TargetedTrimmer(TargetedTrimmer.parse_target_nodes(args.to, graph)) | 
 |         ) | 
 |     if args.single_path: | 
 |         trimmers.append(SinglePathTrimmer()) | 
 |  | 
 |     explorer = BasicExplorer() | 
 |  | 
 |     visitor = DotDumpVisitor( | 
 |         args.diff, args.dark, args.gray, args.topology, args.dump_dot_only | 
 |     ) | 
 |  | 
 |     for trimmer in trimmers: | 
 |         trimmer.trim(graph) | 
 |  | 
 |     explorer.explore(graph, visitor) | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |     main() |