pytact.graph_visualize_browse
1from dataclasses import dataclass, field 2import os 3import graphviz 4from collections import defaultdict 5from abc import ABC, abstractmethod 6from pathlib import Path 7from typing import Union, List, Dict, Set, Tuple, Sequence 8from pytact.data_reader import Dataset, Definition, Node, ProofState, Original, Discharged, Substituted 9import html 10import inflection 11import capnp 12import pytact.graph_api_capnp as graph_api_capnp 13import pytact.graph_api_capnp_cython as apic 14 15class UrlMaker(ABC): 16 17 @abstractmethod 18 def definition(self, fname: Path, defid: int) -> str: 19 pass 20 @abstractmethod 21 def proof(self, fname: Path, defid: int) -> str: 22 pass 23 @abstractmethod 24 def outcome(self, fname: Path, defid: int, stepi: int, outcomei: int) -> str: 25 pass 26 @abstractmethod 27 def global_context(self, fname: Path) -> str: 28 pass 29 @abstractmethod 30 def folder(self, path: Path) -> str: 31 pass 32 @abstractmethod 33 def root_folder(self) -> str: 34 pass 35 36@dataclass 37class Settings: 38 no_defaults: bool = False # Should we use default settings? 39 ignore_edges: List[int] = field(default_factory=lambda: []) 40 unshare_nodes: List[int] = field(default_factory=lambda: []) 41 show_trivial_evar_substs: bool = False 42 hide_proof_terms: bool = False 43 show_edge_labels: bool = False 44 order_edges: bool = False 45 concentrate_edges: bool = False 46 show_non_anonymized_tactics: bool = False 47 max_depth: int = 0 48 max_size: int = 100 49 50 def __post_init__(self): 51 if not self.no_defaults: 52 self.ignore_edges = [graph_api_capnp.EdgeClassification.schema.enumerants['constOpaqueDef']] 53 label = graph_api_capnp.Graph.Node.Label 54 self.unshare_nodes = [label.definition.value, label.sortSProp.value, 55 label.sortProp.value, label.sortSet.value, label.sortType.value] 56 57@dataclass 58class GraphVisualizationData: 59 data: Dict[Path, Dataset] 60 trans_deps: Dict[Path, Set[Path]] = field(init=False) 61 graphid2path: List[Path] = field(init=False) 62 63 def __post_init__(self): 64 self.trans_deps = transitive_closure({d.filename: list(d.dependencies) 65 for d in self.data.values()}) 66 self.graphid2path = [d.filename for d in sorted(self.data.values(), key=lambda d: d.graph)] 67 68@dataclass 69class GraphVisualizationOutput: 70 svg: str 71 location: List[Tuple[str, str]] # tuple[Name, URL] 72 active_location: int 73 text: List[str] = field(default_factory=lambda: []) 74 popups: List[Tuple[str, str]] = field(default_factory=lambda: []) # DOM id, text 75 76def node_label_map(node: Node) -> Tuple[str, str, str]: 77 enum = apic.Graph_Node_Label_Which 78 label = node.label 79 if d := node.definition: 80 name = d.name 81 return ( 82 'box', name.split('.')[-1], 83 f"{inflection.camelize(node.label.definition.which.name.lower())} {d.name}" 84 ) 85 which = label.which 86 if which == enum.SORT_PROP: 87 return 'ellipse', 'Prop', 'SortProp' 88 elif which == enum.SORT_S_PROP: 89 return 'ellipse', 'SProp', 'SortSProp' 90 elif which == enum.SORT_SET: 91 return 'ellipse', 'Set', 'SortSet' 92 elif which == enum.SORT_TYPE: 93 return 'ellipse', 'Type', 'SortType' 94 elif which == enum.REL: 95 return 'circle', '↑', 'rel' 96 elif which == enum.PROD: 97 return 'circle', '∀', 'prod' 98 elif which == enum.LAMBDA: 99 return 'circle', 'λ', 'lambda' 100 elif which == enum.LET_IN: 101 return 'ellipse', 'let', 'LetIn' 102 elif which == enum.APP: 103 return 'circle', '@', 'app' 104 elif which == enum.CASE_BRANCH: 105 return 'ellipse', 'branch', 'CaseBranch' 106 else: 107 name = inflection.camelize(label.which.name.lower()) 108 return 'ellipse', name, name 109 110def truncate_string(data, maximum): 111 return data[:(maximum-2)] + '..' if len(data) > maximum else data 112 113def make_label(context, name): 114 name_split = name.split('.') 115 common = os.path.commonprefix([name_split, context.split('.')]) 116 return '.'.join(name_split[len(common):]) 117 118def graphviz_escape(s): 119 return s.replace('\\', '\\\\') 120 121def make_tooltip(d): 122 return f"{inflection.camelize(d.node.label.definition.which.name.lower())} {d.name}" 123 124def render_proof_state_text(ps: ProofState): 125 return ('<br>'.join(ps.context_text) + 126 '<br>----------------------<br>' + ps.conclusion_text + 127 '<br><br>Raw: ' + ps.text) 128 129class GraphVisualizator: 130 def __init__(self, data: GraphVisualizationData, url_maker: UrlMaker, settings: Settings = Settings()): 131 self.data = data.data 132 self.trans_deps = data.trans_deps 133 self.graphid2path = data.graphid2path 134 self.url_maker = url_maker 135 self.settings = settings 136 self.node_counter = 0 137 138 139 arrow_heads = [ "dot", "inv", "odot", "invdot", "invodot" ] 140 edge_arrow_map = {} 141 for group in graph_api_capnp.groupedEdges: 142 for i, sort in enumerate(group.conflatable): 143 edge_arrow_map[sort.raw] = arrow_heads[i] 144 self.edge_arrow_map = edge_arrow_map 145 146 def url_for_path(self, r: Path): 147 if r in self.data: 148 return r.with_suffix('').parts[-1], self.url_maker.global_context(r) 149 elif len(r.parts) > 0: 150 return r.parts[-1], self.url_maker.folder(r) 151 else: 152 return 'dataset', self.url_maker.root_folder() 153 154 def path2location(self, path: Path): 155 return [self.url_for_path(parent) for parent in list(reversed(path.parents)) + [path]] 156 157 def dot_apply_style(self, dot): 158 dot.attr('node', fontsize='11', fontname="Helvetica", margin='0', height='0.3', 159 style="rounded, filled", penwidth="0") 160 dot.attr('edge', fontsize='11', fontname="Helvetica", arrowsize='0.5', penwidth="0.6") 161 dot.attr('graph', fontsize='11', fontname="Helvetica", penwidth="0.6", ranksep='0.3') 162 if self.settings.order_edges: 163 dot.attr('graph', ordering='out') 164 if self.settings.concentrate_edges: 165 dot.attr('graph', concentrate='true') 166 167 168 def render_node(self, dot, node: Node, shape: str, label: str, id: Union[str, None] = None, 169 tooltip: Union[str, None] = None): 170 if not id: 171 id = str(node) 172 if not tooltip: 173 tooltip = label 174 url = None 175 if node.definition: 176 url = self.url_maker.definition(self.graphid2path[node.graph], node.nodeid) 177 dot.node(id, label, URL = url, shape = shape, tooltip = tooltip) 178 return id 179 180 def render_file_node(self, dot, f: Path): 181 label = f"File: {f.with_suffix('')}" 182 node_id = f"file-{f}" 183 dot.node(node_id, label, URL = self.url_maker.global_context(f), shape='box') 184 return node_id 185 186 def global_context(self, fname: Path): 187 dot = graphviz.Digraph(format='svg') 188 self.dot_apply_style(dot) 189 dot.attr('graph', compound="true") 190 191 dataset = self.data[fname] 192 representative = dataset.representative 193 module_name = dataset.module_name 194 195 def render_def(dot2, d: Definition): 196 label = make_label(module_name, d.name) 197 if representative and representative.node == d.node: 198 label = "Representative: " + label 199 tooltip = make_tooltip(d) 200 if isinstance(d.status, Original): 201 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 202 elif isinstance(d.status, Discharged): 203 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 204 target = d.status.original 205 dot.edge(id, repr(target.node), 206 arrowtail="inv", dir="both", constraint="false", style="dashed") 207 elif isinstance(d.status, Substituted): 208 target = d.status.original 209 if d.node.graph == target.node.graph: 210 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 211 dot.edge(id, str(target.node), 212 arrowtail="odot", dir="both", constraint="false", style="dashed") 213 else: 214 with dot2.subgraph() as dot3: 215 dot3.attr(rank='same') 216 id = self.render_node(dot3, d.node, 'box', label, tooltip=tooltip) 217 id2 = self.render_node(dot3, target.node, 'box', 218 make_label(module_name, target.name), 219 tooltip=make_tooltip(target)) 220 dot.edge(id, id2, 221 arrowtail="odot", dir="both", constraint="false", style="dashed") 222 223 for cluster in dataset.clustered_definitions(): 224 225 start = str(cluster[0].node) 226 ltail = None 227 if len(cluster) == 1: 228 render_def(dot, cluster[0]) 229 else: 230 ltail = "cluster_"+start 231 with dot.subgraph(name=ltail) as dot2: 232 dot2.attr('graph', style='rounded') 233 last = None 234 for d in cluster: 235 render_def(dot2, d) 236 if last: 237 dot2.edge(str(last.node), str(d.node), style="invis") 238 last = d 239 240 if prev := cluster[-1].previous: 241 target = str(prev.node) 242 lhead = None 243 if len(list(prev.cluster)) > 1: 244 lhead = "cluster_"+target 245 dot.edge(str(cluster[-1].node), target, lhead = lhead, ltail = ltail) 246 for fi in cluster[-1].external_previous: 247 fid = self.render_file_node(dot, self.graphid2path[fi.node.graph]) 248 dot.edge(str(cluster[-1].node), fid, ltail = ltail) 249 250 location = self.path2location(fname) 251 return GraphVisualizationOutput(dot.source, location, len(location) - 1) 252 253 def visualize_term(self, dot, start: Node, depth, depth_ignore: Set[Node] = set(), 254 max_nodes=100, seen: Union[Dict[str, str], None]=None, 255 node_label_map=node_label_map, 256 prefix='', before_prefix='', proof_state_prefix: Dict[int, str] = {} 257 ) -> str: 258 if seen == None: 259 seen = {} 260 nodes_left = max_nodes 261 def recurse(node: Node, depth, context_prefix): 262 nonlocal seen 263 nonlocal nodes_left 264 265 enum = graph_api_capnp.Graph.Node.Label 266 which = node.label.which 267 if which == enum.proofState: 268 node_prefix = context_prefix 269 elif which == enum.contextAssum: 270 node_prefix = context_prefix 271 elif which == enum.contextDef: 272 node_prefix = context_prefix 273 elif which == enum.evarSubst: 274 node_prefix = prefix + context_prefix 275 else: 276 node_prefix = prefix 277 id = node_prefix + str(node) 278 if id in seen: 279 return seen[id] 280 dot_id = f"c{self.node_counter}-{id}" 281 seen[id] = dot_id 282 nodes_left -= 1 283 self.node_counter += 1 284 if nodes_left < 0: 285 id = 'trunc' + str(self.node_counter) 286 dot.node(id, 'truncated') 287 return id 288 289 shape, label, tooltip = node_label_map(node) 290 self.render_node(dot, node, shape, label, id=dot_id, tooltip=tooltip) 291 if node.definition and not node in depth_ignore: 292 depth -= 1 293 if depth >= 0: 294 if node.label.which == graph_api_capnp.Graph.Node.Label.evar: 295 # Find the evar-id 296 evarid = [c.label.proof_state.value for _, c in node.children 297 if c.label.which == graph_api_capnp.Graph.Node.Label.proofState][0] 298 context_prefix = proof_state_prefix.get(evarid, context_prefix) 299 300 for edge, child in node.children: 301 if edge in self.settings.ignore_edges: 302 continue 303 if child.label.which == graph_api_capnp.Graph.Node.Label.evarSubst: 304 substs = [s for _, s in child.children] 305 if not self.settings.show_trivial_evar_substs and substs[0] == substs[1]: 306 continue 307 cid = recurse(child, depth, 308 before_prefix if edge == graph_api_capnp.EdgeClassification.evarSubstTerm 309 else context_prefix) 310 edge_name = inflection.camelize(apic.EdgeClassification(edge).name.lower()) 311 if self.settings.show_edge_labels: 312 label = edge_name 313 else: 314 label = "" 315 dot.edge(dot_id, cid, label=label, tooltip=edge_name, labeltooltip=edge_name, 316 arrowtail=self.edge_arrow_map[edge], dir="both") 317 if node.label.which_raw in self.settings.unshare_nodes: 318 del seen[id] 319 return dot_id 320 id = recurse(start, depth, before_prefix) 321 return id 322 323 def definition(self, fname: Path, definition: int): 324 dot = graphviz.Digraph(format='svg') 325 self.dot_apply_style(dot) 326 327 start = self.data[fname].node_by_id(definition) 328 depth_ignore = set() 329 if d := start.definition: 330 depth_ignore = {d.node for d in start.definition.cluster} 331 depth = self.settings.max_depth 332 max_nodes = self.settings.max_size 333 334 self.visualize_term(dot, start, depth=depth, depth_ignore=depth_ignore, 335 max_nodes=max_nodes) 336 337 location = self.path2location(fname) 338 ext_location = location 339 label = "[not a definition]" 340 text = [] 341 proof = [] 342 if d := start.definition: 343 label = d.name 344 text = [f"Type: {d.type_text}"] 345 if term := d.term_text: 346 text.append(f"Term: {term}") 347 if d.proof: 348 proof = [("Proof", self.url_maker.proof(fname, definition))] 349 ext_location = ( 350 location + 351 [(make_label(self.data[fname].module_name, label), 352 self.url_maker.definition(fname, definition))] + 353 proof) 354 return GraphVisualizationOutput(dot.source, ext_location, len(location), text) 355 356 def proof(self, fname: Path, definition: int): 357 node = self.data[fname].node_by_id(definition) 358 d = node.definition 359 if not d: 360 assert False 361 proof = d.proof 362 if not proof: 363 assert False 364 365 dot = graphviz.Digraph(format='svg') 366 self.dot_apply_style(dot) 367 dot.attr('node', style="filled", fillcolor="white", penwidth="0.6") 368 dot.attr('graph', ordering="out") 369 surrogates = set() 370 outcome_to_id = {} 371 for i, step in enumerate(proof): 372 for j, outcome in enumerate(step.outcomes): 373 id = str(outcome.before.id) 374 while id in surrogates: 375 id = id + '-s' 376 surrogates.add(id) 377 outcome_to_id[(i, j)] = id 378 for i, step in enumerate(proof): 379 with dot.subgraph(name='cluster_' + str(i)) as dot2: 380 dot2.attr('graph', labelloc="b", style="rounded") 381 if tactic := step.tactic: 382 if self.settings.show_non_anonymized_tactics: 383 tactic_text = tactic.text_non_anonymous 384 else: 385 tactic_text = tactic.text 386 else: 387 tactic_text = 'unknown' 388 dot2.attr(label=tactic_text) 389 for j, outcome in enumerate(step.outcomes): 390 before_id = outcome_to_id[(i, j)] 391 dot2.node(before_id, label='⬤', shape='circle', fontsize="7", height="0.25pt", 392 URL = self.url_maker.outcome(fname, definition, i, j)) 393 for after in outcome.after: 394 if outcome.before.id == after.id: 395 after_id = before_id + '-s' 396 style = 'dashed' 397 else: 398 after_id = str(after.id) 399 style = 'solid' 400 dot.edge(before_id, after_id, style=style) 401 if not outcome.after: 402 qedid = str('qed-'+str(i)+'-'+str(j)) 403 dot2.node(qedid, label='', shape='point', height='0.05', fillcolor='black') 404 dot.edge(before_id, qedid) 405 406 location = (self.path2location(fname) + 407 [(make_label(self.data[fname].module_name, d.name), 408 self.url_maker.definition(fname, definition)), 409 ("Proof", self.url_maker.proof(fname, definition))]) 410 return GraphVisualizationOutput(dot.source, location, len(location) - 1) 411 412 def outcome(self, fname: Path, definition: int, stepi: int, outcomei: int): 413 depth = self.settings.max_depth 414 max_nodes = self.settings.max_size 415 node = self.data[fname].node_by_id(definition) 416 d = node.definition 417 if not d: 418 assert False 419 proof = d.proof 420 if not proof: 421 assert False 422 423 dot = graphviz.Digraph(format='svg') 424 self.dot_apply_style(dot) 425 426 outcome = proof[stepi].outcomes[outcomei] 427 seen = {} 428 429 def node_label_map_with_ctx_names(context: Sequence[Node], 430 context_text: Sequence[str]): 431 mapping = {n: s for n, s in zip(context, context_text)} 432 def nlm(node: Node): 433 enum = graph_api_capnp.Graph.Node.Label 434 which = node.label.which 435 if which == enum.contextAssum: 436 name = graphviz_escape(mapping[node]) 437 return 'ellipse', truncate_string(name, 20), f"ContextAssum {name}" 438 elif which == enum.contextDef: 439 name = graphviz_escape(mapping[node]) 440 return 'ellipse', truncate_string(name, 20), f"ContextDef {name}" 441 else: 442 return node_label_map(node) 443 return nlm 444 445 popups = [] 446 447 with dot.subgraph(name='cluster_before') as dot2: 448 ps = outcome.before 449 dot2.attr('graph', 450 label=f"Before state\n{graphviz_escape(truncate_string(ps.conclusion_text, 70))}", 451 tooltip=f"Before state {graphviz_escape(ps.conclusion_text)}", 452 id='before-state') 453 popups.append(('before-state', render_proof_state_text(ps))) 454 prefix = 'before' 455 self.visualize_term(dot2, ps.root, depth=depth, prefix=prefix, before_prefix=prefix, 456 max_nodes=max_nodes, seen=seen, 457 node_label_map=node_label_map_with_ctx_names(ps.context, ps.context_text)) 458 459 with dot.subgraph(name='cluster_tactic') as dot2: 460 prefix = 'before' 461 tactic_text = 'unknown' 462 tactic_base_text = 'unknown' 463 if t := outcome.tactic: 464 tactic_text = t.text 465 tactic_base_text = (t.base_text.replace('__argument_marker__', '_') 466 .replace('private_constant_placeholder', '_')) 467 dot2.attr('graph', label=f"Tactic\n{tactic_text}") 468 dot2.node('tactic', label = tactic_base_text) 469 for i, arg in enumerate(outcome.tactic_arguments): 470 if arg is None: 471 dot2.node(f"tactic-arg{i}", label=f"arg {i}: unknown") 472 else: 473 id = self.visualize_term(dot2, arg, depth=depth, prefix=prefix, before_prefix=prefix, 474 max_nodes=max_nodes, seen=seen) 475 dot2.node(f"tactic-arg{i}", label=f"arg {i}") 476 dot2.edge(f"tactic-arg{i}", id) 477 dot2.edge('tactic', f"tactic-arg{i}") 478 479 480 for ai, after in enumerate(outcome.after): 481 with dot.subgraph(name='cluster_after' + str(ai)) as dot2: 482 dot2.attr('graph', 483 label=f"After state {ai}\n{graphviz_escape(truncate_string(after.conclusion_text, 70))}", 484 tooltip=f"After state {ai} {graphviz_escape(after.conclusion_text)}", 485 id=f'after-state{ai}') 486 popups.append((f'after-state{ai}', render_proof_state_text(after))) 487 prefix = f'after{ai}' 488 self.visualize_term(dot2, after.root, depth=depth, prefix=prefix, before_prefix=prefix, 489 max_nodes=max_nodes, seen=seen, 490 node_label_map=node_label_map_with_ctx_names(after.context, after.context_text)) 491 492 if not self.settings.hide_proof_terms: 493 with dot.subgraph(name='cluster_term') as dot2: 494 dot2.attr('graph', 495 label=f"Proof term\n{graphviz_escape(truncate_string(outcome.term_text, 70))}", 496 tooltip=f"Proof term {graphviz_escape(outcome.term_text)}", 497 id='proof-term') 498 popups.append(('proof-term', outcome.term_text)) 499 prefix = 'term' 500 proof_state_prefix = {after.id: f'after{ai}' for ai, after in enumerate(outcome.after)} 501 id = self.visualize_term(dot2, outcome.term, depth=depth, prefix=prefix, before_prefix='before', 502 proof_state_prefix=proof_state_prefix, 503 max_nodes=max_nodes, seen=seen) 504 # Sometimes the subgraph is completely empty because the term is contained in another subgraph. 505 # Therefore, we artificially add a extra root node 506 dot2.node('artificial-root', 'TermRoot') 507 dot2.edge('artificial-root', id) 508 509 location = (self.path2location(fname) + 510 [(make_label(self.data[fname].module_name, d.name), 511 self.url_maker.definition(fname, definition)), 512 ("Proof", self.url_maker.proof(fname, definition)), 513 (f"Step {stepi} outcome {outcomei}", 514 self.url_maker.outcome(fname, definition, stepi, outcomei))]) 515 return GraphVisualizationOutput(dot.source, location, len(location) - 1, [], popups) 516 517 def folder(self, expand_path: Path) -> GraphVisualizationOutput: 518 expand_parts = expand_path.parts 519 520 dot = graphviz.Digraph(engine='dot', format='svg') 521 self.dot_apply_style(dot) 522 523 hierarchy = {'files': [], 'subdirs': {}} 524 for f in self.data: 525 dirs = f.parent.parts 526 leaf = hierarchy 527 for d in dirs: 528 leaf['subdirs'].setdefault(d, {'files': [], 'subdirs': {}}) 529 leaf = leaf['subdirs'][d] 530 leaf['files'].append(f) 531 def common_length(p1, p2): 532 return len(os.path.commonprefix([p1, p2])) 533 def retrieve_edges(rel, h, depth: int): 534 for n in h['files']: 535 deps = {Path(*d.parts[:depth+1]) for d in self.trans_deps[n] 536 if common_length(d.parts, expand_parts) == depth} 537 rel[Path(*n.parts[:depth+1])] |= deps 538 for d in h['subdirs']: 539 retrieve_edges(rel, h['subdirs'][d], depth) 540 return rel 541 def tunnel_hierarchy(dot, h, depth): 542 if depth == len(expand_parts): 543 rel = retrieve_edges(defaultdict(set), h, depth) 544 (rel, repr) = transitive_reduction(rel) 545 for n in rel: 546 if len(repr[n]) == 1: 547 label, url = self.url_for_path(n) 548 shape = 'box' 549 else: 550 label = '<<table border="0" cellborder="0" cellpadding="7">' 551 for r in repr[n]: 552 slabel, surl = self.url_for_path(r) 553 label += f'<tr><td href="{html.escape(surl)}">{slabel}</td></tr>' 554 label += "</table>>" 555 url = None 556 shape = 'plaintext' 557 dot.node(str(n), label, URL = url, shape = shape) 558 for n, deps in rel.items(): 559 for d in deps: 560 dot.edge(str(n), str(d)) 561 else: 562 for d in h['subdirs']: 563 cur_path: Path = Path(*expand_parts[:depth]) / d 564 if depth < len(expand_parts) and d == expand_parts[depth]: 565 cluster_name = 'cluster_' + str(cur_path) 566 with dot.subgraph(name=cluster_name) as dot2: 567 dot2.attr('graph', style="filled", fillcolor="white", label=d, 568 URL = self.url_maker.folder(Path(*expand_parts[:depth+1]))) 569 tunnel_hierarchy(dot2, h['subdirs'][d], depth+1) 570 571 with dot.subgraph(name='cluster_root') as dot2: 572 dot2.attr('graph', style="filled", fillcolor="white", label='dataset', 573 URL = self.url_maker.root_folder()) 574 tunnel_hierarchy(dot2, hierarchy, 0) 575 576 location = self.path2location(expand_path) 577 return GraphVisualizationOutput(dot.source, location, len(location) - 1) 578 579def transitive_closure(rel): 580 trans_deps = defaultdict(set) 581 def calc_trans_deps(a, n): 582 for d in rel[n]: 583 if d not in trans_deps[a]: 584 trans_deps[a].add(d) 585 calc_trans_deps(a, d) 586 for n in list(rel.keys()): 587 calc_trans_deps(n, n) 588 return trans_deps 589 590def transitive_reduction(rel): 591 """This is not a real transitive reduction (NP-hard when it needs to be a subgraph). 592 This is an approximation where all strongly connected components are smashed together 593 in one node. 594 """ 595 trans_deps = transitive_closure(rel) 596 repr_items = defaultdict(set) 597 representative = {} 598 def calc_components(n): 599 if n in representative: 600 return 601 repr_items[n].add(n) 602 representative[n] = n 603 for d in trans_deps[n]: 604 if n in trans_deps[d]: 605 repr_items[n].add(d) 606 representative[d] = n 607 for n in list(rel.keys()): 608 calc_components(n) 609 repr_trans_rel = defaultdict(set) 610 def calc_new_trans_deps(n): 611 for d in trans_deps[n]: 612 if n not in trans_deps[d]: 613 repr_trans_rel[n].add(representative[d]) 614 for n in list(rel.keys()): 615 calc_new_trans_deps(n) 616 def calc_sparse_deps(n): 617 res = set() 618 for d in repr_trans_rel[n]: 619 k = [d2 for d2 in repr_trans_rel[n] 620 if d != d2 and d in trans_deps[d2]] 621 if not k: 622 res.add(d) 623 return res 624 k = {n: calc_sparse_deps(n) for n in repr_items} 625 return (k, repr_items)
class
UrlMaker(abc.ABC):
16class UrlMaker(ABC): 17 18 @abstractmethod 19 def definition(self, fname: Path, defid: int) -> str: 20 pass 21 @abstractmethod 22 def proof(self, fname: Path, defid: int) -> str: 23 pass 24 @abstractmethod 25 def outcome(self, fname: Path, defid: int, stepi: int, outcomei: int) -> str: 26 pass 27 @abstractmethod 28 def global_context(self, fname: Path) -> str: 29 pass 30 @abstractmethod 31 def folder(self, path: Path) -> str: 32 pass 33 @abstractmethod 34 def root_folder(self) -> str: 35 pass
Helper class that provides a standard way to create an ABC using inheritance.
@abstractmethod
def
outcome(self, fname: pathlib.Path, defid: int, stepi: int, outcomei: int) -> str:
@dataclass
class
Settings:
37@dataclass 38class Settings: 39 no_defaults: bool = False # Should we use default settings? 40 ignore_edges: List[int] = field(default_factory=lambda: []) 41 unshare_nodes: List[int] = field(default_factory=lambda: []) 42 show_trivial_evar_substs: bool = False 43 hide_proof_terms: bool = False 44 show_edge_labels: bool = False 45 order_edges: bool = False 46 concentrate_edges: bool = False 47 show_non_anonymized_tactics: bool = False 48 max_depth: int = 0 49 max_size: int = 100 50 51 def __post_init__(self): 52 if not self.no_defaults: 53 self.ignore_edges = [graph_api_capnp.EdgeClassification.schema.enumerants['constOpaqueDef']] 54 label = graph_api_capnp.Graph.Node.Label 55 self.unshare_nodes = [label.definition.value, label.sortSProp.value, 56 label.sortProp.value, label.sortSet.value, label.sortType.value]
Settings( no_defaults: bool = False, ignore_edges: List[int] = <factory>, unshare_nodes: List[int] = <factory>, show_trivial_evar_substs: bool = False, hide_proof_terms: bool = False, show_edge_labels: bool = False, order_edges: bool = False, concentrate_edges: bool = False, show_non_anonymized_tactics: bool = False, max_depth: int = 0, max_size: int = 100)
@dataclass
class
GraphVisualizationData:
58@dataclass 59class GraphVisualizationData: 60 data: Dict[Path, Dataset] 61 trans_deps: Dict[Path, Set[Path]] = field(init=False) 62 graphid2path: List[Path] = field(init=False) 63 64 def __post_init__(self): 65 self.trans_deps = transitive_closure({d.filename: list(d.dependencies) 66 for d in self.data.values()}) 67 self.graphid2path = [d.filename for d in sorted(self.data.values(), key=lambda d: d.graph)]
GraphVisualizationData(data: Dict[pathlib.Path, pytact.data_reader.Dataset])
data: Dict[pathlib.Path, pytact.data_reader.Dataset]
@dataclass
class
GraphVisualizationOutput:
69@dataclass 70class GraphVisualizationOutput: 71 svg: str 72 location: List[Tuple[str, str]] # tuple[Name, URL] 73 active_location: int 74 text: List[str] = field(default_factory=lambda: []) 75 popups: List[Tuple[str, str]] = field(default_factory=lambda: []) # DOM id, text
77def node_label_map(node: Node) -> Tuple[str, str, str]: 78 enum = apic.Graph_Node_Label_Which 79 label = node.label 80 if d := node.definition: 81 name = d.name 82 return ( 83 'box', name.split('.')[-1], 84 f"{inflection.camelize(node.label.definition.which.name.lower())} {d.name}" 85 ) 86 which = label.which 87 if which == enum.SORT_PROP: 88 return 'ellipse', 'Prop', 'SortProp' 89 elif which == enum.SORT_S_PROP: 90 return 'ellipse', 'SProp', 'SortSProp' 91 elif which == enum.SORT_SET: 92 return 'ellipse', 'Set', 'SortSet' 93 elif which == enum.SORT_TYPE: 94 return 'ellipse', 'Type', 'SortType' 95 elif which == enum.REL: 96 return 'circle', '↑', 'rel' 97 elif which == enum.PROD: 98 return 'circle', '∀', 'prod' 99 elif which == enum.LAMBDA: 100 return 'circle', 'λ', 'lambda' 101 elif which == enum.LET_IN: 102 return 'ellipse', 'let', 'LetIn' 103 elif which == enum.APP: 104 return 'circle', '@', 'app' 105 elif which == enum.CASE_BRANCH: 106 return 'ellipse', 'branch', 'CaseBranch' 107 else: 108 name = inflection.camelize(label.which.name.lower()) 109 return 'ellipse', name, name
def
truncate_string(data, maximum):
def
make_label(context, name):
def
graphviz_escape(s):
def
make_tooltip(d):
class
GraphVisualizator:
130class GraphVisualizator: 131 def __init__(self, data: GraphVisualizationData, url_maker: UrlMaker, settings: Settings = Settings()): 132 self.data = data.data 133 self.trans_deps = data.trans_deps 134 self.graphid2path = data.graphid2path 135 self.url_maker = url_maker 136 self.settings = settings 137 self.node_counter = 0 138 139 140 arrow_heads = [ "dot", "inv", "odot", "invdot", "invodot" ] 141 edge_arrow_map = {} 142 for group in graph_api_capnp.groupedEdges: 143 for i, sort in enumerate(group.conflatable): 144 edge_arrow_map[sort.raw] = arrow_heads[i] 145 self.edge_arrow_map = edge_arrow_map 146 147 def url_for_path(self, r: Path): 148 if r in self.data: 149 return r.with_suffix('').parts[-1], self.url_maker.global_context(r) 150 elif len(r.parts) > 0: 151 return r.parts[-1], self.url_maker.folder(r) 152 else: 153 return 'dataset', self.url_maker.root_folder() 154 155 def path2location(self, path: Path): 156 return [self.url_for_path(parent) for parent in list(reversed(path.parents)) + [path]] 157 158 def dot_apply_style(self, dot): 159 dot.attr('node', fontsize='11', fontname="Helvetica", margin='0', height='0.3', 160 style="rounded, filled", penwidth="0") 161 dot.attr('edge', fontsize='11', fontname="Helvetica", arrowsize='0.5', penwidth="0.6") 162 dot.attr('graph', fontsize='11', fontname="Helvetica", penwidth="0.6", ranksep='0.3') 163 if self.settings.order_edges: 164 dot.attr('graph', ordering='out') 165 if self.settings.concentrate_edges: 166 dot.attr('graph', concentrate='true') 167 168 169 def render_node(self, dot, node: Node, shape: str, label: str, id: Union[str, None] = None, 170 tooltip: Union[str, None] = None): 171 if not id: 172 id = str(node) 173 if not tooltip: 174 tooltip = label 175 url = None 176 if node.definition: 177 url = self.url_maker.definition(self.graphid2path[node.graph], node.nodeid) 178 dot.node(id, label, URL = url, shape = shape, tooltip = tooltip) 179 return id 180 181 def render_file_node(self, dot, f: Path): 182 label = f"File: {f.with_suffix('')}" 183 node_id = f"file-{f}" 184 dot.node(node_id, label, URL = self.url_maker.global_context(f), shape='box') 185 return node_id 186 187 def global_context(self, fname: Path): 188 dot = graphviz.Digraph(format='svg') 189 self.dot_apply_style(dot) 190 dot.attr('graph', compound="true") 191 192 dataset = self.data[fname] 193 representative = dataset.representative 194 module_name = dataset.module_name 195 196 def render_def(dot2, d: Definition): 197 label = make_label(module_name, d.name) 198 if representative and representative.node == d.node: 199 label = "Representative: " + label 200 tooltip = make_tooltip(d) 201 if isinstance(d.status, Original): 202 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 203 elif isinstance(d.status, Discharged): 204 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 205 target = d.status.original 206 dot.edge(id, repr(target.node), 207 arrowtail="inv", dir="both", constraint="false", style="dashed") 208 elif isinstance(d.status, Substituted): 209 target = d.status.original 210 if d.node.graph == target.node.graph: 211 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 212 dot.edge(id, str(target.node), 213 arrowtail="odot", dir="both", constraint="false", style="dashed") 214 else: 215 with dot2.subgraph() as dot3: 216 dot3.attr(rank='same') 217 id = self.render_node(dot3, d.node, 'box', label, tooltip=tooltip) 218 id2 = self.render_node(dot3, target.node, 'box', 219 make_label(module_name, target.name), 220 tooltip=make_tooltip(target)) 221 dot.edge(id, id2, 222 arrowtail="odot", dir="both", constraint="false", style="dashed") 223 224 for cluster in dataset.clustered_definitions(): 225 226 start = str(cluster[0].node) 227 ltail = None 228 if len(cluster) == 1: 229 render_def(dot, cluster[0]) 230 else: 231 ltail = "cluster_"+start 232 with dot.subgraph(name=ltail) as dot2: 233 dot2.attr('graph', style='rounded') 234 last = None 235 for d in cluster: 236 render_def(dot2, d) 237 if last: 238 dot2.edge(str(last.node), str(d.node), style="invis") 239 last = d 240 241 if prev := cluster[-1].previous: 242 target = str(prev.node) 243 lhead = None 244 if len(list(prev.cluster)) > 1: 245 lhead = "cluster_"+target 246 dot.edge(str(cluster[-1].node), target, lhead = lhead, ltail = ltail) 247 for fi in cluster[-1].external_previous: 248 fid = self.render_file_node(dot, self.graphid2path[fi.node.graph]) 249 dot.edge(str(cluster[-1].node), fid, ltail = ltail) 250 251 location = self.path2location(fname) 252 return GraphVisualizationOutput(dot.source, location, len(location) - 1) 253 254 def visualize_term(self, dot, start: Node, depth, depth_ignore: Set[Node] = set(), 255 max_nodes=100, seen: Union[Dict[str, str], None]=None, 256 node_label_map=node_label_map, 257 prefix='', before_prefix='', proof_state_prefix: Dict[int, str] = {} 258 ) -> str: 259 if seen == None: 260 seen = {} 261 nodes_left = max_nodes 262 def recurse(node: Node, depth, context_prefix): 263 nonlocal seen 264 nonlocal nodes_left 265 266 enum = graph_api_capnp.Graph.Node.Label 267 which = node.label.which 268 if which == enum.proofState: 269 node_prefix = context_prefix 270 elif which == enum.contextAssum: 271 node_prefix = context_prefix 272 elif which == enum.contextDef: 273 node_prefix = context_prefix 274 elif which == enum.evarSubst: 275 node_prefix = prefix + context_prefix 276 else: 277 node_prefix = prefix 278 id = node_prefix + str(node) 279 if id in seen: 280 return seen[id] 281 dot_id = f"c{self.node_counter}-{id}" 282 seen[id] = dot_id 283 nodes_left -= 1 284 self.node_counter += 1 285 if nodes_left < 0: 286 id = 'trunc' + str(self.node_counter) 287 dot.node(id, 'truncated') 288 return id 289 290 shape, label, tooltip = node_label_map(node) 291 self.render_node(dot, node, shape, label, id=dot_id, tooltip=tooltip) 292 if node.definition and not node in depth_ignore: 293 depth -= 1 294 if depth >= 0: 295 if node.label.which == graph_api_capnp.Graph.Node.Label.evar: 296 # Find the evar-id 297 evarid = [c.label.proof_state.value for _, c in node.children 298 if c.label.which == graph_api_capnp.Graph.Node.Label.proofState][0] 299 context_prefix = proof_state_prefix.get(evarid, context_prefix) 300 301 for edge, child in node.children: 302 if edge in self.settings.ignore_edges: 303 continue 304 if child.label.which == graph_api_capnp.Graph.Node.Label.evarSubst: 305 substs = [s for _, s in child.children] 306 if not self.settings.show_trivial_evar_substs and substs[0] == substs[1]: 307 continue 308 cid = recurse(child, depth, 309 before_prefix if edge == graph_api_capnp.EdgeClassification.evarSubstTerm 310 else context_prefix) 311 edge_name = inflection.camelize(apic.EdgeClassification(edge).name.lower()) 312 if self.settings.show_edge_labels: 313 label = edge_name 314 else: 315 label = "" 316 dot.edge(dot_id, cid, label=label, tooltip=edge_name, labeltooltip=edge_name, 317 arrowtail=self.edge_arrow_map[edge], dir="both") 318 if node.label.which_raw in self.settings.unshare_nodes: 319 del seen[id] 320 return dot_id 321 id = recurse(start, depth, before_prefix) 322 return id 323 324 def definition(self, fname: Path, definition: int): 325 dot = graphviz.Digraph(format='svg') 326 self.dot_apply_style(dot) 327 328 start = self.data[fname].node_by_id(definition) 329 depth_ignore = set() 330 if d := start.definition: 331 depth_ignore = {d.node for d in start.definition.cluster} 332 depth = self.settings.max_depth 333 max_nodes = self.settings.max_size 334 335 self.visualize_term(dot, start, depth=depth, depth_ignore=depth_ignore, 336 max_nodes=max_nodes) 337 338 location = self.path2location(fname) 339 ext_location = location 340 label = "[not a definition]" 341 text = [] 342 proof = [] 343 if d := start.definition: 344 label = d.name 345 text = [f"Type: {d.type_text}"] 346 if term := d.term_text: 347 text.append(f"Term: {term}") 348 if d.proof: 349 proof = [("Proof", self.url_maker.proof(fname, definition))] 350 ext_location = ( 351 location + 352 [(make_label(self.data[fname].module_name, label), 353 self.url_maker.definition(fname, definition))] + 354 proof) 355 return GraphVisualizationOutput(dot.source, ext_location, len(location), text) 356 357 def proof(self, fname: Path, definition: int): 358 node = self.data[fname].node_by_id(definition) 359 d = node.definition 360 if not d: 361 assert False 362 proof = d.proof 363 if not proof: 364 assert False 365 366 dot = graphviz.Digraph(format='svg') 367 self.dot_apply_style(dot) 368 dot.attr('node', style="filled", fillcolor="white", penwidth="0.6") 369 dot.attr('graph', ordering="out") 370 surrogates = set() 371 outcome_to_id = {} 372 for i, step in enumerate(proof): 373 for j, outcome in enumerate(step.outcomes): 374 id = str(outcome.before.id) 375 while id in surrogates: 376 id = id + '-s' 377 surrogates.add(id) 378 outcome_to_id[(i, j)] = id 379 for i, step in enumerate(proof): 380 with dot.subgraph(name='cluster_' + str(i)) as dot2: 381 dot2.attr('graph', labelloc="b", style="rounded") 382 if tactic := step.tactic: 383 if self.settings.show_non_anonymized_tactics: 384 tactic_text = tactic.text_non_anonymous 385 else: 386 tactic_text = tactic.text 387 else: 388 tactic_text = 'unknown' 389 dot2.attr(label=tactic_text) 390 for j, outcome in enumerate(step.outcomes): 391 before_id = outcome_to_id[(i, j)] 392 dot2.node(before_id, label='⬤', shape='circle', fontsize="7", height="0.25pt", 393 URL = self.url_maker.outcome(fname, definition, i, j)) 394 for after in outcome.after: 395 if outcome.before.id == after.id: 396 after_id = before_id + '-s' 397 style = 'dashed' 398 else: 399 after_id = str(after.id) 400 style = 'solid' 401 dot.edge(before_id, after_id, style=style) 402 if not outcome.after: 403 qedid = str('qed-'+str(i)+'-'+str(j)) 404 dot2.node(qedid, label='', shape='point', height='0.05', fillcolor='black') 405 dot.edge(before_id, qedid) 406 407 location = (self.path2location(fname) + 408 [(make_label(self.data[fname].module_name, d.name), 409 self.url_maker.definition(fname, definition)), 410 ("Proof", self.url_maker.proof(fname, definition))]) 411 return GraphVisualizationOutput(dot.source, location, len(location) - 1) 412 413 def outcome(self, fname: Path, definition: int, stepi: int, outcomei: int): 414 depth = self.settings.max_depth 415 max_nodes = self.settings.max_size 416 node = self.data[fname].node_by_id(definition) 417 d = node.definition 418 if not d: 419 assert False 420 proof = d.proof 421 if not proof: 422 assert False 423 424 dot = graphviz.Digraph(format='svg') 425 self.dot_apply_style(dot) 426 427 outcome = proof[stepi].outcomes[outcomei] 428 seen = {} 429 430 def node_label_map_with_ctx_names(context: Sequence[Node], 431 context_text: Sequence[str]): 432 mapping = {n: s for n, s in zip(context, context_text)} 433 def nlm(node: Node): 434 enum = graph_api_capnp.Graph.Node.Label 435 which = node.label.which 436 if which == enum.contextAssum: 437 name = graphviz_escape(mapping[node]) 438 return 'ellipse', truncate_string(name, 20), f"ContextAssum {name}" 439 elif which == enum.contextDef: 440 name = graphviz_escape(mapping[node]) 441 return 'ellipse', truncate_string(name, 20), f"ContextDef {name}" 442 else: 443 return node_label_map(node) 444 return nlm 445 446 popups = [] 447 448 with dot.subgraph(name='cluster_before') as dot2: 449 ps = outcome.before 450 dot2.attr('graph', 451 label=f"Before state\n{graphviz_escape(truncate_string(ps.conclusion_text, 70))}", 452 tooltip=f"Before state {graphviz_escape(ps.conclusion_text)}", 453 id='before-state') 454 popups.append(('before-state', render_proof_state_text(ps))) 455 prefix = 'before' 456 self.visualize_term(dot2, ps.root, depth=depth, prefix=prefix, before_prefix=prefix, 457 max_nodes=max_nodes, seen=seen, 458 node_label_map=node_label_map_with_ctx_names(ps.context, ps.context_text)) 459 460 with dot.subgraph(name='cluster_tactic') as dot2: 461 prefix = 'before' 462 tactic_text = 'unknown' 463 tactic_base_text = 'unknown' 464 if t := outcome.tactic: 465 tactic_text = t.text 466 tactic_base_text = (t.base_text.replace('__argument_marker__', '_') 467 .replace('private_constant_placeholder', '_')) 468 dot2.attr('graph', label=f"Tactic\n{tactic_text}") 469 dot2.node('tactic', label = tactic_base_text) 470 for i, arg in enumerate(outcome.tactic_arguments): 471 if arg is None: 472 dot2.node(f"tactic-arg{i}", label=f"arg {i}: unknown") 473 else: 474 id = self.visualize_term(dot2, arg, depth=depth, prefix=prefix, before_prefix=prefix, 475 max_nodes=max_nodes, seen=seen) 476 dot2.node(f"tactic-arg{i}", label=f"arg {i}") 477 dot2.edge(f"tactic-arg{i}", id) 478 dot2.edge('tactic', f"tactic-arg{i}") 479 480 481 for ai, after in enumerate(outcome.after): 482 with dot.subgraph(name='cluster_after' + str(ai)) as dot2: 483 dot2.attr('graph', 484 label=f"After state {ai}\n{graphviz_escape(truncate_string(after.conclusion_text, 70))}", 485 tooltip=f"After state {ai} {graphviz_escape(after.conclusion_text)}", 486 id=f'after-state{ai}') 487 popups.append((f'after-state{ai}', render_proof_state_text(after))) 488 prefix = f'after{ai}' 489 self.visualize_term(dot2, after.root, depth=depth, prefix=prefix, before_prefix=prefix, 490 max_nodes=max_nodes, seen=seen, 491 node_label_map=node_label_map_with_ctx_names(after.context, after.context_text)) 492 493 if not self.settings.hide_proof_terms: 494 with dot.subgraph(name='cluster_term') as dot2: 495 dot2.attr('graph', 496 label=f"Proof term\n{graphviz_escape(truncate_string(outcome.term_text, 70))}", 497 tooltip=f"Proof term {graphviz_escape(outcome.term_text)}", 498 id='proof-term') 499 popups.append(('proof-term', outcome.term_text)) 500 prefix = 'term' 501 proof_state_prefix = {after.id: f'after{ai}' for ai, after in enumerate(outcome.after)} 502 id = self.visualize_term(dot2, outcome.term, depth=depth, prefix=prefix, before_prefix='before', 503 proof_state_prefix=proof_state_prefix, 504 max_nodes=max_nodes, seen=seen) 505 # Sometimes the subgraph is completely empty because the term is contained in another subgraph. 506 # Therefore, we artificially add a extra root node 507 dot2.node('artificial-root', 'TermRoot') 508 dot2.edge('artificial-root', id) 509 510 location = (self.path2location(fname) + 511 [(make_label(self.data[fname].module_name, d.name), 512 self.url_maker.definition(fname, definition)), 513 ("Proof", self.url_maker.proof(fname, definition)), 514 (f"Step {stepi} outcome {outcomei}", 515 self.url_maker.outcome(fname, definition, stepi, outcomei))]) 516 return GraphVisualizationOutput(dot.source, location, len(location) - 1, [], popups) 517 518 def folder(self, expand_path: Path) -> GraphVisualizationOutput: 519 expand_parts = expand_path.parts 520 521 dot = graphviz.Digraph(engine='dot', format='svg') 522 self.dot_apply_style(dot) 523 524 hierarchy = {'files': [], 'subdirs': {}} 525 for f in self.data: 526 dirs = f.parent.parts 527 leaf = hierarchy 528 for d in dirs: 529 leaf['subdirs'].setdefault(d, {'files': [], 'subdirs': {}}) 530 leaf = leaf['subdirs'][d] 531 leaf['files'].append(f) 532 def common_length(p1, p2): 533 return len(os.path.commonprefix([p1, p2])) 534 def retrieve_edges(rel, h, depth: int): 535 for n in h['files']: 536 deps = {Path(*d.parts[:depth+1]) for d in self.trans_deps[n] 537 if common_length(d.parts, expand_parts) == depth} 538 rel[Path(*n.parts[:depth+1])] |= deps 539 for d in h['subdirs']: 540 retrieve_edges(rel, h['subdirs'][d], depth) 541 return rel 542 def tunnel_hierarchy(dot, h, depth): 543 if depth == len(expand_parts): 544 rel = retrieve_edges(defaultdict(set), h, depth) 545 (rel, repr) = transitive_reduction(rel) 546 for n in rel: 547 if len(repr[n]) == 1: 548 label, url = self.url_for_path(n) 549 shape = 'box' 550 else: 551 label = '<<table border="0" cellborder="0" cellpadding="7">' 552 for r in repr[n]: 553 slabel, surl = self.url_for_path(r) 554 label += f'<tr><td href="{html.escape(surl)}">{slabel}</td></tr>' 555 label += "</table>>" 556 url = None 557 shape = 'plaintext' 558 dot.node(str(n), label, URL = url, shape = shape) 559 for n, deps in rel.items(): 560 for d in deps: 561 dot.edge(str(n), str(d)) 562 else: 563 for d in h['subdirs']: 564 cur_path: Path = Path(*expand_parts[:depth]) / d 565 if depth < len(expand_parts) and d == expand_parts[depth]: 566 cluster_name = 'cluster_' + str(cur_path) 567 with dot.subgraph(name=cluster_name) as dot2: 568 dot2.attr('graph', style="filled", fillcolor="white", label=d, 569 URL = self.url_maker.folder(Path(*expand_parts[:depth+1]))) 570 tunnel_hierarchy(dot2, h['subdirs'][d], depth+1) 571 572 with dot.subgraph(name='cluster_root') as dot2: 573 dot2.attr('graph', style="filled", fillcolor="white", label='dataset', 574 URL = self.url_maker.root_folder()) 575 tunnel_hierarchy(dot2, hierarchy, 0) 576 577 location = self.path2location(expand_path) 578 return GraphVisualizationOutput(dot.source, location, len(location) - 1)
GraphVisualizator( data: GraphVisualizationData, url_maker: UrlMaker, settings: Settings = Settings(no_defaults=False, ignore_edges=[7], unshare_nodes=[3, 5, 6, 7, 8], show_trivial_evar_substs=False, hide_proof_terms=False, show_edge_labels=False, order_edges=False, concentrate_edges=False, show_non_anonymized_tactics=False, max_depth=0, max_size=100))
131 def __init__(self, data: GraphVisualizationData, url_maker: UrlMaker, settings: Settings = Settings()): 132 self.data = data.data 133 self.trans_deps = data.trans_deps 134 self.graphid2path = data.graphid2path 135 self.url_maker = url_maker 136 self.settings = settings 137 self.node_counter = 0 138 139 140 arrow_heads = [ "dot", "inv", "odot", "invdot", "invodot" ] 141 edge_arrow_map = {} 142 for group in graph_api_capnp.groupedEdges: 143 for i, sort in enumerate(group.conflatable): 144 edge_arrow_map[sort.raw] = arrow_heads[i] 145 self.edge_arrow_map = edge_arrow_map
def
dot_apply_style(self, dot):
158 def dot_apply_style(self, dot): 159 dot.attr('node', fontsize='11', fontname="Helvetica", margin='0', height='0.3', 160 style="rounded, filled", penwidth="0") 161 dot.attr('edge', fontsize='11', fontname="Helvetica", arrowsize='0.5', penwidth="0.6") 162 dot.attr('graph', fontsize='11', fontname="Helvetica", penwidth="0.6", ranksep='0.3') 163 if self.settings.order_edges: 164 dot.attr('graph', ordering='out') 165 if self.settings.concentrate_edges: 166 dot.attr('graph', concentrate='true')
def
render_node( self, dot, node: pytact.data_reader.Node, shape: str, label: str, id: Optional[str] = None, tooltip: Optional[str] = None):
169 def render_node(self, dot, node: Node, shape: str, label: str, id: Union[str, None] = None, 170 tooltip: Union[str, None] = None): 171 if not id: 172 id = str(node) 173 if not tooltip: 174 tooltip = label 175 url = None 176 if node.definition: 177 url = self.url_maker.definition(self.graphid2path[node.graph], node.nodeid) 178 dot.node(id, label, URL = url, shape = shape, tooltip = tooltip) 179 return id
def
global_context(self, fname: pathlib.Path):
187 def global_context(self, fname: Path): 188 dot = graphviz.Digraph(format='svg') 189 self.dot_apply_style(dot) 190 dot.attr('graph', compound="true") 191 192 dataset = self.data[fname] 193 representative = dataset.representative 194 module_name = dataset.module_name 195 196 def render_def(dot2, d: Definition): 197 label = make_label(module_name, d.name) 198 if representative and representative.node == d.node: 199 label = "Representative: " + label 200 tooltip = make_tooltip(d) 201 if isinstance(d.status, Original): 202 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 203 elif isinstance(d.status, Discharged): 204 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 205 target = d.status.original 206 dot.edge(id, repr(target.node), 207 arrowtail="inv", dir="both", constraint="false", style="dashed") 208 elif isinstance(d.status, Substituted): 209 target = d.status.original 210 if d.node.graph == target.node.graph: 211 id = self.render_node(dot2, d.node, 'box', label, tooltip=tooltip) 212 dot.edge(id, str(target.node), 213 arrowtail="odot", dir="both", constraint="false", style="dashed") 214 else: 215 with dot2.subgraph() as dot3: 216 dot3.attr(rank='same') 217 id = self.render_node(dot3, d.node, 'box', label, tooltip=tooltip) 218 id2 = self.render_node(dot3, target.node, 'box', 219 make_label(module_name, target.name), 220 tooltip=make_tooltip(target)) 221 dot.edge(id, id2, 222 arrowtail="odot", dir="both", constraint="false", style="dashed") 223 224 for cluster in dataset.clustered_definitions(): 225 226 start = str(cluster[0].node) 227 ltail = None 228 if len(cluster) == 1: 229 render_def(dot, cluster[0]) 230 else: 231 ltail = "cluster_"+start 232 with dot.subgraph(name=ltail) as dot2: 233 dot2.attr('graph', style='rounded') 234 last = None 235 for d in cluster: 236 render_def(dot2, d) 237 if last: 238 dot2.edge(str(last.node), str(d.node), style="invis") 239 last = d 240 241 if prev := cluster[-1].previous: 242 target = str(prev.node) 243 lhead = None 244 if len(list(prev.cluster)) > 1: 245 lhead = "cluster_"+target 246 dot.edge(str(cluster[-1].node), target, lhead = lhead, ltail = ltail) 247 for fi in cluster[-1].external_previous: 248 fid = self.render_file_node(dot, self.graphid2path[fi.node.graph]) 249 dot.edge(str(cluster[-1].node), fid, ltail = ltail) 250 251 location = self.path2location(fname) 252 return GraphVisualizationOutput(dot.source, location, len(location) - 1)
def
visualize_term( self, dot, start: pytact.data_reader.Node, depth, depth_ignore: Set[pytact.data_reader.Node] = set(), max_nodes=100, seen: Optional[Dict[str, str]] = None, node_label_map=<function node_label_map>, prefix='', before_prefix='', proof_state_prefix: Dict[int, str] = {}) -> str:
254 def visualize_term(self, dot, start: Node, depth, depth_ignore: Set[Node] = set(), 255 max_nodes=100, seen: Union[Dict[str, str], None]=None, 256 node_label_map=node_label_map, 257 prefix='', before_prefix='', proof_state_prefix: Dict[int, str] = {} 258 ) -> str: 259 if seen == None: 260 seen = {} 261 nodes_left = max_nodes 262 def recurse(node: Node, depth, context_prefix): 263 nonlocal seen 264 nonlocal nodes_left 265 266 enum = graph_api_capnp.Graph.Node.Label 267 which = node.label.which 268 if which == enum.proofState: 269 node_prefix = context_prefix 270 elif which == enum.contextAssum: 271 node_prefix = context_prefix 272 elif which == enum.contextDef: 273 node_prefix = context_prefix 274 elif which == enum.evarSubst: 275 node_prefix = prefix + context_prefix 276 else: 277 node_prefix = prefix 278 id = node_prefix + str(node) 279 if id in seen: 280 return seen[id] 281 dot_id = f"c{self.node_counter}-{id}" 282 seen[id] = dot_id 283 nodes_left -= 1 284 self.node_counter += 1 285 if nodes_left < 0: 286 id = 'trunc' + str(self.node_counter) 287 dot.node(id, 'truncated') 288 return id 289 290 shape, label, tooltip = node_label_map(node) 291 self.render_node(dot, node, shape, label, id=dot_id, tooltip=tooltip) 292 if node.definition and not node in depth_ignore: 293 depth -= 1 294 if depth >= 0: 295 if node.label.which == graph_api_capnp.Graph.Node.Label.evar: 296 # Find the evar-id 297 evarid = [c.label.proof_state.value for _, c in node.children 298 if c.label.which == graph_api_capnp.Graph.Node.Label.proofState][0] 299 context_prefix = proof_state_prefix.get(evarid, context_prefix) 300 301 for edge, child in node.children: 302 if edge in self.settings.ignore_edges: 303 continue 304 if child.label.which == graph_api_capnp.Graph.Node.Label.evarSubst: 305 substs = [s for _, s in child.children] 306 if not self.settings.show_trivial_evar_substs and substs[0] == substs[1]: 307 continue 308 cid = recurse(child, depth, 309 before_prefix if edge == graph_api_capnp.EdgeClassification.evarSubstTerm 310 else context_prefix) 311 edge_name = inflection.camelize(apic.EdgeClassification(edge).name.lower()) 312 if self.settings.show_edge_labels: 313 label = edge_name 314 else: 315 label = "" 316 dot.edge(dot_id, cid, label=label, tooltip=edge_name, labeltooltip=edge_name, 317 arrowtail=self.edge_arrow_map[edge], dir="both") 318 if node.label.which_raw in self.settings.unshare_nodes: 319 del seen[id] 320 return dot_id 321 id = recurse(start, depth, before_prefix) 322 return id
def
definition(self, fname: pathlib.Path, definition: int):
324 def definition(self, fname: Path, definition: int): 325 dot = graphviz.Digraph(format='svg') 326 self.dot_apply_style(dot) 327 328 start = self.data[fname].node_by_id(definition) 329 depth_ignore = set() 330 if d := start.definition: 331 depth_ignore = {d.node for d in start.definition.cluster} 332 depth = self.settings.max_depth 333 max_nodes = self.settings.max_size 334 335 self.visualize_term(dot, start, depth=depth, depth_ignore=depth_ignore, 336 max_nodes=max_nodes) 337 338 location = self.path2location(fname) 339 ext_location = location 340 label = "[not a definition]" 341 text = [] 342 proof = [] 343 if d := start.definition: 344 label = d.name 345 text = [f"Type: {d.type_text}"] 346 if term := d.term_text: 347 text.append(f"Term: {term}") 348 if d.proof: 349 proof = [("Proof", self.url_maker.proof(fname, definition))] 350 ext_location = ( 351 location + 352 [(make_label(self.data[fname].module_name, label), 353 self.url_maker.definition(fname, definition))] + 354 proof) 355 return GraphVisualizationOutput(dot.source, ext_location, len(location), text)
def
proof(self, fname: pathlib.Path, definition: int):
357 def proof(self, fname: Path, definition: int): 358 node = self.data[fname].node_by_id(definition) 359 d = node.definition 360 if not d: 361 assert False 362 proof = d.proof 363 if not proof: 364 assert False 365 366 dot = graphviz.Digraph(format='svg') 367 self.dot_apply_style(dot) 368 dot.attr('node', style="filled", fillcolor="white", penwidth="0.6") 369 dot.attr('graph', ordering="out") 370 surrogates = set() 371 outcome_to_id = {} 372 for i, step in enumerate(proof): 373 for j, outcome in enumerate(step.outcomes): 374 id = str(outcome.before.id) 375 while id in surrogates: 376 id = id + '-s' 377 surrogates.add(id) 378 outcome_to_id[(i, j)] = id 379 for i, step in enumerate(proof): 380 with dot.subgraph(name='cluster_' + str(i)) as dot2: 381 dot2.attr('graph', labelloc="b", style="rounded") 382 if tactic := step.tactic: 383 if self.settings.show_non_anonymized_tactics: 384 tactic_text = tactic.text_non_anonymous 385 else: 386 tactic_text = tactic.text 387 else: 388 tactic_text = 'unknown' 389 dot2.attr(label=tactic_text) 390 for j, outcome in enumerate(step.outcomes): 391 before_id = outcome_to_id[(i, j)] 392 dot2.node(before_id, label='⬤', shape='circle', fontsize="7", height="0.25pt", 393 URL = self.url_maker.outcome(fname, definition, i, j)) 394 for after in outcome.after: 395 if outcome.before.id == after.id: 396 after_id = before_id + '-s' 397 style = 'dashed' 398 else: 399 after_id = str(after.id) 400 style = 'solid' 401 dot.edge(before_id, after_id, style=style) 402 if not outcome.after: 403 qedid = str('qed-'+str(i)+'-'+str(j)) 404 dot2.node(qedid, label='', shape='point', height='0.05', fillcolor='black') 405 dot.edge(before_id, qedid) 406 407 location = (self.path2location(fname) + 408 [(make_label(self.data[fname].module_name, d.name), 409 self.url_maker.definition(fname, definition)), 410 ("Proof", self.url_maker.proof(fname, definition))]) 411 return GraphVisualizationOutput(dot.source, location, len(location) - 1)
def
outcome( self, fname: pathlib.Path, definition: int, stepi: int, outcomei: int):
413 def outcome(self, fname: Path, definition: int, stepi: int, outcomei: int): 414 depth = self.settings.max_depth 415 max_nodes = self.settings.max_size 416 node = self.data[fname].node_by_id(definition) 417 d = node.definition 418 if not d: 419 assert False 420 proof = d.proof 421 if not proof: 422 assert False 423 424 dot = graphviz.Digraph(format='svg') 425 self.dot_apply_style(dot) 426 427 outcome = proof[stepi].outcomes[outcomei] 428 seen = {} 429 430 def node_label_map_with_ctx_names(context: Sequence[Node], 431 context_text: Sequence[str]): 432 mapping = {n: s for n, s in zip(context, context_text)} 433 def nlm(node: Node): 434 enum = graph_api_capnp.Graph.Node.Label 435 which = node.label.which 436 if which == enum.contextAssum: 437 name = graphviz_escape(mapping[node]) 438 return 'ellipse', truncate_string(name, 20), f"ContextAssum {name}" 439 elif which == enum.contextDef: 440 name = graphviz_escape(mapping[node]) 441 return 'ellipse', truncate_string(name, 20), f"ContextDef {name}" 442 else: 443 return node_label_map(node) 444 return nlm 445 446 popups = [] 447 448 with dot.subgraph(name='cluster_before') as dot2: 449 ps = outcome.before 450 dot2.attr('graph', 451 label=f"Before state\n{graphviz_escape(truncate_string(ps.conclusion_text, 70))}", 452 tooltip=f"Before state {graphviz_escape(ps.conclusion_text)}", 453 id='before-state') 454 popups.append(('before-state', render_proof_state_text(ps))) 455 prefix = 'before' 456 self.visualize_term(dot2, ps.root, depth=depth, prefix=prefix, before_prefix=prefix, 457 max_nodes=max_nodes, seen=seen, 458 node_label_map=node_label_map_with_ctx_names(ps.context, ps.context_text)) 459 460 with dot.subgraph(name='cluster_tactic') as dot2: 461 prefix = 'before' 462 tactic_text = 'unknown' 463 tactic_base_text = 'unknown' 464 if t := outcome.tactic: 465 tactic_text = t.text 466 tactic_base_text = (t.base_text.replace('__argument_marker__', '_') 467 .replace('private_constant_placeholder', '_')) 468 dot2.attr('graph', label=f"Tactic\n{tactic_text}") 469 dot2.node('tactic', label = tactic_base_text) 470 for i, arg in enumerate(outcome.tactic_arguments): 471 if arg is None: 472 dot2.node(f"tactic-arg{i}", label=f"arg {i}: unknown") 473 else: 474 id = self.visualize_term(dot2, arg, depth=depth, prefix=prefix, before_prefix=prefix, 475 max_nodes=max_nodes, seen=seen) 476 dot2.node(f"tactic-arg{i}", label=f"arg {i}") 477 dot2.edge(f"tactic-arg{i}", id) 478 dot2.edge('tactic', f"tactic-arg{i}") 479 480 481 for ai, after in enumerate(outcome.after): 482 with dot.subgraph(name='cluster_after' + str(ai)) as dot2: 483 dot2.attr('graph', 484 label=f"After state {ai}\n{graphviz_escape(truncate_string(after.conclusion_text, 70))}", 485 tooltip=f"After state {ai} {graphviz_escape(after.conclusion_text)}", 486 id=f'after-state{ai}') 487 popups.append((f'after-state{ai}', render_proof_state_text(after))) 488 prefix = f'after{ai}' 489 self.visualize_term(dot2, after.root, depth=depth, prefix=prefix, before_prefix=prefix, 490 max_nodes=max_nodes, seen=seen, 491 node_label_map=node_label_map_with_ctx_names(after.context, after.context_text)) 492 493 if not self.settings.hide_proof_terms: 494 with dot.subgraph(name='cluster_term') as dot2: 495 dot2.attr('graph', 496 label=f"Proof term\n{graphviz_escape(truncate_string(outcome.term_text, 70))}", 497 tooltip=f"Proof term {graphviz_escape(outcome.term_text)}", 498 id='proof-term') 499 popups.append(('proof-term', outcome.term_text)) 500 prefix = 'term' 501 proof_state_prefix = {after.id: f'after{ai}' for ai, after in enumerate(outcome.after)} 502 id = self.visualize_term(dot2, outcome.term, depth=depth, prefix=prefix, before_prefix='before', 503 proof_state_prefix=proof_state_prefix, 504 max_nodes=max_nodes, seen=seen) 505 # Sometimes the subgraph is completely empty because the term is contained in another subgraph. 506 # Therefore, we artificially add a extra root node 507 dot2.node('artificial-root', 'TermRoot') 508 dot2.edge('artificial-root', id) 509 510 location = (self.path2location(fname) + 511 [(make_label(self.data[fname].module_name, d.name), 512 self.url_maker.definition(fname, definition)), 513 ("Proof", self.url_maker.proof(fname, definition)), 514 (f"Step {stepi} outcome {outcomei}", 515 self.url_maker.outcome(fname, definition, stepi, outcomei))]) 516 return GraphVisualizationOutput(dot.source, location, len(location) - 1, [], popups)
518 def folder(self, expand_path: Path) -> GraphVisualizationOutput: 519 expand_parts = expand_path.parts 520 521 dot = graphviz.Digraph(engine='dot', format='svg') 522 self.dot_apply_style(dot) 523 524 hierarchy = {'files': [], 'subdirs': {}} 525 for f in self.data: 526 dirs = f.parent.parts 527 leaf = hierarchy 528 for d in dirs: 529 leaf['subdirs'].setdefault(d, {'files': [], 'subdirs': {}}) 530 leaf = leaf['subdirs'][d] 531 leaf['files'].append(f) 532 def common_length(p1, p2): 533 return len(os.path.commonprefix([p1, p2])) 534 def retrieve_edges(rel, h, depth: int): 535 for n in h['files']: 536 deps = {Path(*d.parts[:depth+1]) for d in self.trans_deps[n] 537 if common_length(d.parts, expand_parts) == depth} 538 rel[Path(*n.parts[:depth+1])] |= deps 539 for d in h['subdirs']: 540 retrieve_edges(rel, h['subdirs'][d], depth) 541 return rel 542 def tunnel_hierarchy(dot, h, depth): 543 if depth == len(expand_parts): 544 rel = retrieve_edges(defaultdict(set), h, depth) 545 (rel, repr) = transitive_reduction(rel) 546 for n in rel: 547 if len(repr[n]) == 1: 548 label, url = self.url_for_path(n) 549 shape = 'box' 550 else: 551 label = '<<table border="0" cellborder="0" cellpadding="7">' 552 for r in repr[n]: 553 slabel, surl = self.url_for_path(r) 554 label += f'<tr><td href="{html.escape(surl)}">{slabel}</td></tr>' 555 label += "</table>>" 556 url = None 557 shape = 'plaintext' 558 dot.node(str(n), label, URL = url, shape = shape) 559 for n, deps in rel.items(): 560 for d in deps: 561 dot.edge(str(n), str(d)) 562 else: 563 for d in h['subdirs']: 564 cur_path: Path = Path(*expand_parts[:depth]) / d 565 if depth < len(expand_parts) and d == expand_parts[depth]: 566 cluster_name = 'cluster_' + str(cur_path) 567 with dot.subgraph(name=cluster_name) as dot2: 568 dot2.attr('graph', style="filled", fillcolor="white", label=d, 569 URL = self.url_maker.folder(Path(*expand_parts[:depth+1]))) 570 tunnel_hierarchy(dot2, h['subdirs'][d], depth+1) 571 572 with dot.subgraph(name='cluster_root') as dot2: 573 dot2.attr('graph', style="filled", fillcolor="white", label='dataset', 574 URL = self.url_maker.root_folder()) 575 tunnel_hierarchy(dot2, hierarchy, 0) 576 577 location = self.path2location(expand_path) 578 return GraphVisualizationOutput(dot.source, location, len(location) - 1)
def
transitive_closure(rel):
def
transitive_reduction(rel):
591def transitive_reduction(rel): 592 """This is not a real transitive reduction (NP-hard when it needs to be a subgraph). 593 This is an approximation where all strongly connected components are smashed together 594 in one node. 595 """ 596 trans_deps = transitive_closure(rel) 597 repr_items = defaultdict(set) 598 representative = {} 599 def calc_components(n): 600 if n in representative: 601 return 602 repr_items[n].add(n) 603 representative[n] = n 604 for d in trans_deps[n]: 605 if n in trans_deps[d]: 606 repr_items[n].add(d) 607 representative[d] = n 608 for n in list(rel.keys()): 609 calc_components(n) 610 repr_trans_rel = defaultdict(set) 611 def calc_new_trans_deps(n): 612 for d in trans_deps[n]: 613 if n not in trans_deps[d]: 614 repr_trans_rel[n].add(representative[d]) 615 for n in list(rel.keys()): 616 calc_new_trans_deps(n) 617 def calc_sparse_deps(n): 618 res = set() 619 for d in repr_trans_rel[n]: 620 k = [d2 for d2 in repr_trans_rel[n] 621 if d != d2 and d in trans_deps[d2]] 622 if not k: 623 res.add(d) 624 return res 625 k = {n: calc_sparse_deps(n) for n in repr_items} 626 return (k, repr_items)
This is not a real transitive reduction (NP-hard when it needs to be a subgraph). This is an approximation where all strongly connected components are smashed together in one node.