1#!/usr/bin/env python3.8 2 3""" Convert a grammar into a dot-file suitable for use with GraphViz 4 5 For example: 6 Generate the GraphViz file: 7 # scripts/grammar_grapher.py data/python.gram > python.gv 8 9 Then generate the graph... 10 11 # twopi python.gv -Tpng > python_twopi.png 12 13 or 14 15 # dot python.gv -Tpng > python_dot.png 16 17 NOTE: The _dot_ and _twopi_ tools seem to produce the most useful results. 18 The _circo_ tool is the worst of the bunch. Don't even bother. 19""" 20 21import argparse 22import sys 23 24from typing import Any, List 25 26sys.path.insert(0, ".") 27 28from pegen.build import build_parser 29from pegen.grammar import ( 30 Alt, 31 Cut, 32 Forced, 33 Grammar, 34 Group, 35 Leaf, 36 Lookahead, 37 Rule, 38 NameLeaf, 39 NamedItem, 40 Opt, 41 Repeat, 42 Rhs, 43) 44 45argparser = argparse.ArgumentParser( 46 prog="graph_grammar", 47 description="Graph a grammar tree", 48) 49argparser.add_argument( 50 "-s", 51 "--start", 52 choices=["exec", "eval", "single"], 53 default="exec", 54 help="Choose the grammar's start rule (exec, eval or single)", 55) 56argparser.add_argument("grammar_file", help="The grammar file to graph") 57 58 59def references_for_item(item: Any) -> List[Any]: 60 if isinstance(item, Alt): 61 return [_ref for _item in item.items for _ref in references_for_item(_item)] 62 elif isinstance(item, Cut): 63 return [] 64 elif isinstance(item, Forced): 65 return references_for_item(item.node) 66 elif isinstance(item, Group): 67 return references_for_item(item.rhs) 68 elif isinstance(item, Lookahead): 69 return references_for_item(item.node) 70 elif isinstance(item, NamedItem): 71 return references_for_item(item.item) 72 73 # NOTE NameLeaf must be before Leaf 74 elif isinstance(item, NameLeaf): 75 if item.value == "ENDMARKER": 76 return [] 77 return [item.value] 78 elif isinstance(item, Leaf): 79 return [] 80 81 elif isinstance(item, Opt): 82 return references_for_item(item.node) 83 elif isinstance(item, Repeat): 84 return references_for_item(item.node) 85 elif isinstance(item, Rhs): 86 return [_ref for alt in item.alts for _ref in references_for_item(alt)] 87 elif isinstance(item, Rule): 88 return references_for_item(item.rhs) 89 else: 90 raise RuntimeError(f"Unknown item: {type(item)}") 91 92 93def main() -> None: 94 args = argparser.parse_args() 95 96 try: 97 grammar, parser, tokenizer = build_parser(args.grammar_file) 98 except Exception as err: 99 print("ERROR: Failed to parse grammar file", file=sys.stderr) 100 sys.exit(1) 101 102 references = {} 103 for name, rule in grammar.rules.items(): 104 references[name] = set(references_for_item(rule)) 105 106 # Flatten the start node if has only a single reference 107 root_node = {"exec": "file", "eval": "eval", "single": "interactive"}[args.start] 108 109 print("digraph g1 {") 110 print('\toverlap="scale";') # Force twopi to scale the graph to avoid overlaps 111 print(f'\troot="{root_node}";') 112 print(f"\t{root_node} [color=green, shape=circle];") 113 for name, refs in references.items(): 114 for ref in refs: 115 print(f"\t{name} -> {ref};") 116 print("}") 117 118 119if __name__ == "__main__": 120 main() 121