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
21 import argparse
22 import sys
23
24 from typing import Any, List
25
26 sys.path.insert(0, ".")
27
28 from pegen.build import build_parser
29 from pegen.grammar import (
30 Alt,
31 Cut,
32 Forced,
33 Group,
34 Leaf,
35 Lookahead,
36 Rule,
37 NameLeaf,
38 NamedItem,
39 Opt,
40 Repeat,
41 Rhs,
42 )
43
44 argparser = argparse.ArgumentParser(
45 prog="graph_grammar",
46 description="Graph a grammar tree",
47 )
48 argparser.add_argument(
49 "-s",
50 "--start",
51 choices=["exec", "eval", "single"],
52 default="exec",
53 help="Choose the grammar's start rule (exec, eval or single)",
54 )
55 argparser.add_argument("grammar_file", help="The grammar file to graph")
56
57
58 def references_for_item(item: Any) -> List[Any]:
59 if isinstance(item, Alt):
60 return [_ref for _item in item.items for _ref in references_for_item(_item)]
61 elif isinstance(item, Cut):
62 return []
63 elif isinstance(item, Forced):
64 return references_for_item(item.node)
65 elif isinstance(item, Group):
66 return references_for_item(item.rhs)
67 elif isinstance(item, Lookahead):
68 return references_for_item(item.node)
69 elif isinstance(item, NamedItem):
70 return references_for_item(item.item)
71
72 # NOTE NameLeaf must be before Leaf
73 elif isinstance(item, NameLeaf):
74 if item.value == "ENDMARKER":
75 return []
76 return [item.value]
77 elif isinstance(item, Leaf):
78 return []
79
80 elif isinstance(item, Opt):
81 return references_for_item(item.node)
82 elif isinstance(item, Repeat):
83 return references_for_item(item.node)
84 elif isinstance(item, Rhs):
85 return [_ref for alt in item.alts for _ref in references_for_item(alt)]
86 elif isinstance(item, Rule):
87 return references_for_item(item.rhs)
88 else:
89 raise RuntimeError(f"Unknown item: {type(item)}")
90
91
92 def main() -> None:
93 args = argparser.parse_args()
94
95 try:
96 grammar, parser, tokenizer = build_parser(args.grammar_file)
97 except Exception as err:
98 print("ERROR: Failed to parse grammar file", file=sys.stderr)
99 sys.exit(1)
100
101 references = {}
102 for name, rule in grammar.rules.items():
103 references[name] = set(references_for_item(rule))
104
105 # Flatten the start node if has only a single reference
106 root_node = {"exec": "file", "eval": "eval", "single": "interactive"}[args.start]
107
108 print("digraph g1 {")
109 print('\toverlap="scale";') # Force twopi to scale the graph to avoid overlaps
110 print(f'\troot="{root_node}";')
111 print(f"\t{root_node} [color=green, shape=circle];")
112 for name, refs in references.items():
113 for ref in refs:
114 print(f"\t{name} -> {ref};")
115 print("}")
116
117
118 if __name__ == "__main__":
119 main()