115 lines
3.1 KiB
Python
Executable File
115 lines
3.1 KiB
Python
Executable File
#!/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,
|
|
Grammar,
|
|
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, 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()
|