diff options
Diffstat (limited to 'doc/scripts/gen_state_diagram.py')
-rwxr-xr-x | doc/scripts/gen_state_diagram.py | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/doc/scripts/gen_state_diagram.py b/doc/scripts/gen_state_diagram.py new file mode 100755 index 000000000..b084f8a74 --- /dev/null +++ b/doc/scripts/gen_state_diagram.py @@ -0,0 +1,242 @@ +#!/usr/bin/python3 + +import itertools +import re +import sys + + +def do_filter(generator): + return acc_lines(remove_multiline_comments(to_char(remove_single_line_comments(generator)))) + + +def acc_lines(generator): + current = "" + for i in generator: + current += i + if i == ';' or \ + i == '{' or \ + i == '}': + yield current.lstrip("\n") + current = "" + + +def to_char(generator): + for line in generator: + for char in line: + if char != '\n': + yield char + else: + yield ' ' + + +def remove_single_line_comments(generator): + for i in generator: + if len(i) and i[0] == '#': + continue + yield re.sub(r'//.*', '', i) + + +def remove_multiline_comments(generator): + saw = "" + in_comment = False + for char in generator: + if in_comment: + if saw == "*": + if char == "/": + in_comment = False + saw = "" + if char == "*": + saw = "*" + continue + if saw == "/": + if char == '*': + in_comment = True + saw = "" + continue + else: + yield saw + saw = "" + if char == '/': + saw = "/" + continue + yield char + + +class StateMachineRenderer(object): + def __init__(self): + self.states = {} # state -> parent + self.machines = {} # state-> initial + self.edges = {} # event -> [(state, state)] + + self.context = [] # [(context, depth_encountered)] + self.context_depth = 0 + self.state_contents = {} + self.subgraphnum = 0 + self.clusterlabel = {} + + self.color_palette = itertools.cycle([ + "#000000", # black + "#1e90ff", # dodgerblue + "#ff0000", # red + "#0000ff", # blue + "#ffa500", # orange + "#40e0d0", # turquoise + "#c71585", # mediumvioletred + ]) + + def __str__(self): + return f'''------------------- + + states: {self.states} + + machines: {self.machines} + + edges: {self.edges} + + context: {self.context} + + state_contents: {self.state_contents} + +--------------------''' + + def read_input(self, input_lines): + previous_line = None + for line in input_lines: + self.get_state(line) + self.get_event(line) + # pass two lines at a time to get the context so that regexes can + # match on split signatures + self.get_context(line, previous_line) + previous_line = line + + def get_context(self, line, previous_line): + match = re.search(r"(\w+::)*::(?P<tag>\w+)::\w+\(const (?P<event>\w+)", line) + if match is None and previous_line is not None: + # it is possible that we need to match on the previous line as well, so join + # them to make them one line and try and get this matching + joined_line = ' '.join([previous_line, line]) + match = re.search(r"(\w+::)*::(?P<tag>\w+)::\w+\(\s*const (?P<event>\w+)", joined_line) + if match is not None: + self.context.append((match.group('tag'), self.context_depth, match.group('event'))) + if '{' in line: + self.context_depth += 1 + if '}' in line: + self.context_depth -= 1 + while len(self.context) and self.context[-1][1] == self.context_depth: + self.context.pop() + + def get_state(self, line): + if "boost::statechart::state_machine" in line: + tokens = re.search( + r"boost::statechart::state_machine<\s*(\w*),\s*(\w*)\s*>", + line) + if tokens is None: + raise Exception("Error: malformed state_machine line: " + line) + self.machines[tokens.group(1)] = tokens.group(2) + self.context.append((tokens.group(1), self.context_depth, "")) + return + if "boost::statechart::state" in line: + tokens = re.search( + r"boost::statechart::state<\s*(\w*),\s*(\w*)\s*,?\s*(\w*)\s*>", + line) + if tokens is None: + raise Exception("Error: malformed state line: " + line) + self.states[tokens.group(1)] = tokens.group(2) + if tokens.group(2) not in self.state_contents.keys(): + self.state_contents[tokens.group(2)] = [] + self.state_contents[tokens.group(2)].append(tokens.group(1)) + if tokens.group(3): + self.machines[tokens.group(1)] = tokens.group(3) + self.context.append((tokens.group(1), self.context_depth, "")) + return + + def get_event(self, line): + if "boost::statechart::transition" in line: + for i in re.finditer(r'boost::statechart::transition<\s*([\w:]*)\s*,\s*(\w*)\s*>', + line): + if i.group(1) not in self.edges.keys(): + self.edges[i.group(1)] = [] + if not self.context: + raise Exception("no context at line: " + line) + self.edges[i.group(1)].append((self.context[-1][0], i.group(2))) + i = re.search("return\s+transit<\s*(\w*)\s*>()", line) + if i is not None: + if not self.context: + raise Exception("no context at line: " + line) + if not self.context[-1][2]: + raise Exception("no event in context at line: " + line) + if self.context[-1][2] not in self.edges.keys(): + self.edges[self.context[-1][2]] = [] + self.edges[self.context[-1][2]].append((self.context[-1][0], i.group(1))) + + def emit_dot(self, output): + top_level = [] + for state in self.machines.keys(): + if state not in self.states.keys(): + top_level.append(state) + print('Top Level States: ', top_level, file=sys.stderr) + print('digraph G {', file=output) + print('\tsize="7,7"', file=output) + print('\tcompound=true;', file=output) + for i in self.emit_state(top_level[0]): + print('\t' + i, file=output) + for i in self.edges.keys(): + for j in self.emit_event(i): + print(j, file=output) + print('}', file=output) + + def emit_state(self, state): + if state in self.state_contents.keys(): + self.clusterlabel[state] = "cluster%s" % (str(self.subgraphnum),) + yield "subgraph cluster%s {" % (str(self.subgraphnum),) + self.subgraphnum += 1 + yield """\tlabel = "%s";""" % (state,) + yield """\tcolor = "black";""" + + if state in self.machines.values(): + yield """\tstyle = "filled";""" + yield """\tfillcolor = "lightgrey";""" + + for j in self.state_contents[state]: + for i in self.emit_state(j): + yield "\t"+i + yield "}" + else: + found = False + for (k, v) in self.machines.items(): + if v == state: + yield state+"[shape=Mdiamond style=filled fillcolor=lightgrey];" + found = True + break + if not found: + yield state+";" + + def emit_event(self, event): + def append(app): + retval = "[" + for i in app: + retval += (i + ",") + retval += "]" + return retval + + for (fro, to) in self.edges[event]: + color = next(self.color_palette) + appendix = ['label="%s"' % (event,), + 'color="%s"' % (color,), + 'fontcolor="%s"' % (color,)] + if fro in self.machines.keys(): + appendix.append("ltail=%s" % (self.clusterlabel[fro],)) + while fro in self.machines.keys(): + fro = self.machines[fro] + if to in self.machines.keys(): + appendix.append("lhead=%s" % (self.clusterlabel[to],)) + while to in self.machines.keys(): + to = self.machines[to] + yield("%s -> %s %s;" % (fro, to, append(appendix))) + + +if __name__ == '__main__': + INPUT_GENERATOR = do_filter(line for line in sys.stdin) + RENDERER = StateMachineRenderer() + RENDERER.read_input(INPUT_GENERATOR) + RENDERER.emit_dot(output=sys.stdout) |