#!/usr/bin/env python3.8 """ Convert a grammar into a dot-file suitable for use with GraphViz For example: Generate the GraphViz file: # scripts/grammar_grapher.py data/python.gram > python.gv Then generate the graph... # twopi python.gv -Tpng > python_twopi.png or # dot python.gv -Tpng > python_dot.png NOTE: The _dot_ and _twopi_ tools seem to produce the most useful results. The _circo_ tool is the worst of the bunch. Don't even bother. """ import argparse import sys from typing import Any, List sys.path.insert(0, ".") from pegen.build import build_parser from pegen.grammar import ( Alt, Cut, Forced, Group, Leaf, Lookahead, Rule, NameLeaf, NamedItem, Opt, Repeat, Rhs, ) argparser = argparse.ArgumentParser( prog="graph_grammar", description="Graph a grammar tree", ) argparser.add_argument( "-s", "--start", choices=["exec", "eval", "single"], default="exec", help="Choose the grammar's start rule (exec, eval or single)", ) argparser.add_argument("grammar_file", help="The grammar file to graph") def references_for_item(item: Any) -> List[Any]: if isinstance(item, Alt): return [_ref for _item in item.items for _ref in references_for_item(_item)] elif isinstance(item, Cut): return [] elif isinstance(item, Forced): return references_for_item(item.node) elif isinstance(item, Group): return references_for_item(item.rhs) elif isinstance(item, Lookahead): return references_for_item(item.node) elif isinstance(item, NamedItem): return references_for_item(item.item) # NOTE NameLeaf must be before Leaf elif isinstance(item, NameLeaf): if item.value == "ENDMARKER": return [] return [item.value] elif isinstance(item, Leaf): return [] elif isinstance(item, Opt): return references_for_item(item.node) elif isinstance(item, Repeat): return references_for_item(item.node) elif isinstance(item, Rhs): return [_ref for alt in item.alts for _ref in references_for_item(alt)] elif isinstance(item, Rule): return references_for_item(item.rhs) else: raise RuntimeError(f"Unknown item: {type(item)}") def main() -> None: args = argparser.parse_args() try: grammar, parser, tokenizer = build_parser(args.grammar_file) except Exception as err: print("ERROR: Failed to parse grammar file", file=sys.stderr) sys.exit(1) references = {} for name, rule in grammar.rules.items(): references[name] = set(references_for_item(rule)) # Flatten the start node if has only a single reference root_node = {"exec": "file", "eval": "eval", "single": "interactive"}[args.start] print("digraph g1 {") print('\toverlap="scale";') # Force twopi to scale the graph to avoid overlaps print(f'\troot="{root_node}";') print(f"\t{root_node} [color=green, shape=circle];") for name, refs in references.items(): for ref in refs: print(f"\t{name} -> {ref};") print("}") if __name__ == "__main__": main()