blob: 06d2df10c24fe30e0bc601c1884bf40210b3d321 [file] [log] [blame]
Neels Hofmeyr0898a002016-11-16 14:36:29 +01001#!/usr/bin/env python
2
3__doc__ = '''
4fsm-to-dot: convert FSM definitons to graph images
5
6Usage:
7 ./fsm-to-dot.py ~/openbsc/openbsc/src/libvlr/*.c
Neels Hofmeyra568af22017-10-23 04:03:19 +02008 for f in *.dot ; do dot -Tpng "$f" > "$f.png"; done
Neels Hofmeyr0898a002016-11-16 14:36:29 +01009 # dot comes from 'apt-get install graphviz'
10
11Looks for osmo_fsm finite state machine definitions and madly parses .c files
12to draw graphs of them. This uses wild regexes that rely on coding style etc..
13No proper C parsing is done here (pycparser sucked, unfortunately).
14'''
15
Neels Hofmeyra568af22017-10-23 04:03:19 +020016import sys, re, os
Neels Hofmeyr0898a002016-11-16 14:36:29 +010017
18def err(msg):
19 sys.stderr.write(msg + '\n')
20
21class listdict(object):
22 def __getattr__(ld, name):
23 if name == 'add':
24 return ld.__getattribute__(name)
25 return ld.__dict__.__getattribute__(name)
26
27 def _have(ld, name):
28 l = ld.__dict__.get(name)
29 if not l:
30 l = []
31 ld.__dict__[name] = l
32 return l
33
34 def add(ld, name, item):
35 l = ld._have(name)
36 l.append(item)
37 return ld
38
39 def add_dict(ld, d):
40 for k,v in d.items():
41 ld.add(k, v)
42
43 def __setitem__(ld, name, val):
44 return ld.__dict__.__setitem__(name, val)
45
46 def __getitem__(ld, name):
47 return ld.__dict__.__getitem__(name)
48
49 def __str__(ld):
50 return ld.__dict__.__str__()
51
52 def __repr__(ld):
53 return ld.__dict__.__repr__()
54
55 def update(ld, other_ld):
56 for name, items in other_ld.items():
57 ld.extend(name, items)
58 return ld
59
60 def extend(ld, name, vals):
61 l = ld._have(name)
62 l.extend(vals)
63 return ld
64
65re_state_start = re.compile(r'\[([A-Z_][A-Z_0-9]*)\]')
Neels Hofmeyra568af22017-10-23 04:03:19 +020066re_event_alternatives = [
67 re.compile(r'\(1 *<< *([A-Z_][A-Z_0-9]*)\)'),
68 re.compile(r'S\(([A-Z_][A-Z_0-9]*)\)'),
69 ]
Neels Hofmeyr0898a002016-11-16 14:36:29 +010070re_action = re.compile(r'.action *= *([a-z_][a-z_0-9]*)')
71
Neels Hofmeyra568af22017-10-23 04:03:19 +020072re_insane_dot_name_chars = re.compile('[^a-zA-Z_]')
73
Neels Hofmeyr0898a002016-11-16 14:36:29 +010074def state_starts(line):
75 m = re_state_start.search(line)
76 if m:
77 return m.group(1)
78 return None
79
80def in_event_starts(line):
81 return line.find('in_event_mask') >= 0
82
83def out_state_starts(line):
84 return line.find('out_state_mask') >= 0
85
86def states_or_events(line):
Neels Hofmeyra568af22017-10-23 04:03:19 +020087 results = []
88 for one_re in re_event_alternatives:
89 results.extend(one_re.findall(line))
90 return results
Neels Hofmeyr0898a002016-11-16 14:36:29 +010091
92def parse_action(line):
93 a = re_action.findall(line)
94 if a:
95 return a[0]
96 return None
97
98def _common_prefix(a, b):
99 for l in reversed(range(1,len(a))):
100 aa = a[:l+1]
101 if b.startswith(aa):
102 return aa
103 return ''
104
105def common_prefix(strs):
106 if not strs:
107 return ''
108 p = None
109 for s in strs:
110 if p is None:
111 p = s
112 continue
113 p = _common_prefix(p, s)
114 if not p:
115 return ''
116 return p
117
118KIND_STATE = 'KIND_STATE'
119KIND_FUNC = 'KIND_FUNC'
120KIND_FSM = 'KIND_FSM'
121BOX_SHAPES = {
122 KIND_STATE : None,
123 KIND_FUNC : 'box',
124 KIND_FSM : 'box3d',
125}
126
127class Event:
128 def __init__(event, name):
129 event.name = name
130 event.short_name = name
131
132 def __cmp__(event, other):
133 return cmp(event.name, other.name)
134
135class Edge:
136 def __init__(edge, to_state, event_name=None, style=None, action=None):
137 edge.to_state = to_state
138 edge.style = style
139 edge.events = []
140 edge.actions = []
141 edge.add_event_name(event_name)
142 edge.add_action(action)
143
144 def add_event_name(edge, event_name):
145 if not event_name:
146 return
147 edge.add_event(Event(event_name))
148
149 def add_event(edge, event):
150 if not event:
151 return
152 if event in edge.events:
153 return
154 edge.events.append(event)
155
156 def add_events(edge, events):
157 for event in events:
158 edge.add_event(event)
159
160 def add_action(edge, action):
161 if not action or action in edge.actions:
162 return
163 edge.actions.append(action)
164
165 def add_actions(edge, actions):
166 for action in actions:
167 edge.add_action(action)
168
169 def event_names(edge):
170 return sorted([event.name for event in edge.events])
171
172 def event_labels(edge):
173 return sorted([event.short_name for event in edge.events])
174
175 def action_labels(edge):
176 return sorted([action + '()' for action in edge.actions])
177
178 def has_event_name(edge, event_name):
179 return event_name in edge.event_names()
180
181class State:
182 name = None
183 short_name = None
184 action = None
185 label = None
186 in_event_names = None
187 out_state_names = None
188 out_edges = None
189 kind = None
190
191 def __init__(state):
192 state.in_event_names = []
193 state.out_state_names = []
194 state.out_edges = []
195 state.kind = KIND_STATE
196
197 def add_out_edge(state, edge):
198 for out_edge in state.out_edges:
199 if out_edge.to_state is edge.to_state:
200 if out_edge.style == edge.style:
201 out_edge.add_events(edge.events)
202 out_edge.add_actions(edge.actions)
203 return
204 # sanity
205 if out_edge.to_state.get_label() == edge.to_state.get_label():
206 raise Exception('Two distinct states exist with identical labels.')
207 state.out_edges.append(edge)
208
209 def get_label(state):
210 if state.label:
211 return state.label
212 l = [state.short_name]
213 if state.action:
214 if state.short_name == state.action:
215 l = []
216 l.append(state.action + '()')
217 return r'\n'.join(l)
218
219 def event_names(state):
220 event_names = []
221 for out_edge in state.out_edges:
222 event_names.extend(out_edge.event_names())
223 return event_names
224
225 def shape_str(state):
226 shape = BOX_SHAPES.get(state.kind, None)
227 if not shape:
228 return ''
229 return ',shape=%s' % shape
230
231 def __repr__(state):
232 return 'State(name=%r,short_name=%r,out=%d)' % (state.name, state.short_name, len(state.out_edges))
233
234class Fsm:
Neels Hofmeyra568af22017-10-23 04:03:19 +0200235 def __init__(fsm, struct_name, string_name, states_struct_name, from_file=None):
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100236 fsm.states = []
237 fsm.struct_name = struct_name
Neels Hofmeyra568af22017-10-23 04:03:19 +0200238 fsm.string_name = string_name
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100239 fsm.states_struct_name = states_struct_name
240 fsm.from_file = from_file
241 fsm.action_funcs = set()
242 fsm.event_names = set()
Neels Hofmeyra568af22017-10-23 04:03:19 +0200243 fsm.dot_name = fsm.all_names_sanitized()
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100244
245 def parse_states(fsm, src):
246 state = None
247 started = None
248
249 IN_EVENTS = 'events'
250 OUT_STATES = 'states'
251
252 lines = src.splitlines()
253
254 for line in lines:
255 state_name = state_starts(line)
256 if state_name:
257 state = State()
258 fsm.states.append(state)
259 started = None
260 state.name = state_name
261
262 if in_event_starts(line):
263 started = IN_EVENTS
264 if out_state_starts(line):
265 started = OUT_STATES
266
267 if not state or not started:
268 continue
269
270 tokens = states_or_events(line)
271 if started == IN_EVENTS:
272 state.in_event_names.extend(tokens)
273 elif started == OUT_STATES:
274 state.out_state_names.extend(tokens)
275 else:
276 err('ignoring: %r' % tokens)
277
278 a = parse_action(line)
279 if a:
280 state.action = a
281
282
283 for state in fsm.states:
284 if state.action:
285 fsm.action_funcs.add(state.action)
286 if state.in_event_names:
287 fsm.event_names.update(state.in_event_names)
288
289 fsm.make_states_short_names()
290 fsm.ref_out_states()
291
292 def make_states_short_names(fsm):
293 p = common_prefix([s.name for s in fsm.states])
294 for s in fsm.states:
295 s.short_name = s.name[len(p):]
296 return p
297
298 def make_events_short_names(fsm):
299 p = common_prefix(fsm.event_names)
300 for state in fsm.states:
301 for edge in state.out_edges:
302 for event in edge.events:
303 event.short_name = event.name[len(p):]
304
305 def ref_out_states(fsm):
306 for state in fsm.states:
307 for e in [Edge(fsm.find_state_by_name(n, True)) for n in state.out_state_names]:
308 state.add_out_edge(e)
309
310 def find_state_by_name(fsm, name, strict=False):
311 for state in fsm.states:
312 if state.name == name:
313 return state
314 if strict:
315 raise Exception("State not found: %r" % name);
316 return None
317
318 def find_state_by_action(fsm, action):
319 for state in fsm.states:
320 if state.action == action:
321 return state
322 return None
323
324 def add_special_state(fsm, additional_states, name, in_state=None,
325 out_state=None, event_name=None, kind=KIND_FUNC,
326 state_action=None, label=None, edge_action=None):
327 additional_state = None
328 for s in additional_states:
329 if s.short_name == name:
330 additional_state = s
331 break;
332
333 if not additional_state:
334 for s in fsm.states:
335 if s.short_name == name:
336 additional_state = s
337 break;
338
339 if kind == KIND_FUNC and not state_action:
340 state_action = name
341
342 if not additional_state:
343 additional_state = State()
344 additional_state.short_name = name
345 additional_state.action = state_action
346 additional_state.kind = kind
347 additional_state.label = label
348 additional_states.append(additional_state)
349
350 if out_state:
351 additional_state.out_state_names.append(out_state.name)
352 additional_state.add_out_edge(Edge(out_state, event_name, 'dotted',
353 action=edge_action))
354
355 if in_state:
356 in_state.out_state_names.append(additional_state.name)
357 in_state.add_out_edge(Edge(additional_state, event_name, 'dotted',
358 action=edge_action))
359
360
361 def find_event_edges(fsm, c_files):
362 # enrich state transitions between the states with event labels
363 func_to_state_transitions = listdict()
364 for c_file in c_files:
365 func_to_state_transitions.update( c_file.find_state_transitions(fsm.event_names) )
366
367 # edges between explicit states
368 for state in fsm.states:
369 transitions = func_to_state_transitions.get(state.action)
370 if not transitions:
371 continue
372
373 for to_state_name, event_name in transitions:
374 if not event_name:
375 continue
376 for out_edge in state.out_edges:
377 if out_edge.to_state.name == to_state_name:
378 out_edge.add_event_name(event_name)
379
380 additional_states = []
381
382
383 # functions that aren't state actions but still effect state transitions
384 for func_name, transitions in func_to_state_transitions.items():
385 if func_name in fsm.action_funcs:
386 continue
387 for to_state_name, event_name in transitions:
388 to_state = fsm.find_state_by_name(to_state_name)
389 if not to_state:
390 continue
391 fsm.add_special_state(additional_states, func_name, None, to_state, event_name)
392
393
394 event_sources = c_files.find_event_sources(fsm.event_names)
395
396 for state in fsm.states:
397
398 for in_event_name in state.in_event_names:
399 funcs_for_in_event = event_sources.get(in_event_name)
400 if not funcs_for_in_event:
401 continue
402
403 found = False
404 for out_edge in state.out_edges:
405 if out_edge.has_event_name(in_event_name):
406 out_edge.action = r'\n'.join([(f + '()') for f in funcs_for_in_event
407 if f != state.action])
408
409 # if any functions that don't belong to a state trigger events, add
410 # them to the graph as well
411 additional_funcs = [f for f in funcs_for_in_event if f not in fsm.action_funcs]
412 for af in additional_funcs:
413 fsm.add_special_state(additional_states, af, None, state, in_event_name)
414
415 fsm.states.extend(additional_states)
416
417 # do any existing action functions by chance call other action functions?
418 for state in fsm.states:
419 if not state.action:
420 continue
421 callers = c_files.find_callers(state.action)
422 if not callers:
423 continue
424 for other_state in fsm.states:
425 if other_state.action in callers:
426 other_state.add_out_edge(Edge(state, None, 'dotted'))
427
428 def add_fsm_alloc(fsm, c_files):
429
430 allocating_funcs = []
431 for c_file in c_files:
432 allocating_funcs.extend(c_file.fsm_allocators.get(fsm.struct_name, []))
433
434 starting_state = None
435 if fsm.states:
436 # assume the first state starts
437 starting_state = fsm.states[0]
438
439 additional_states = []
440 for func_name in allocating_funcs:
441 fsm.add_special_state(additional_states, func_name, None, starting_state)
442
443 fsm.states.extend(additional_states)
444
445 def add_cross_fsm_links(fsm, fsms, c_files, fsm_meta):
446 for state in fsm.states:
447 if not state.action:
448 continue
449 if state.kind == KIND_FSM:
450 continue
451 callers = c_files.find_callers(state.action)
452
453 if state.kind == KIND_FUNC:
454 callers.append(state.action)
455
456 if not callers:
457 continue
458
459 for caller in callers:
460 for calling_fsm in fsms:
461 if calling_fsm is fsm:
462 continue
463 calling_state = calling_fsm.find_state_by_action(caller)
464 if not calling_state:
465 continue
466 if calling_state.kind == KIND_FSM:
467 continue
468
469 label = None
470 if state.kind == KIND_STATE:
471 label=fsm.struct_name + ': ' + state.short_name
472 edge_action = caller
473 if calling_state.action == edge_action:
474 edge_action = None
Neels Hofmeyra568af22017-10-23 04:03:19 +0200475 calling_fsm.add_special_state(calling_fsm.states, fsm.dot_name,
476 calling_state, kind=KIND_FSM, edge_action=edge_action, label=' '.join(fsm.all_names()))
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100477
478 label = None
479 if calling_state.kind == KIND_STATE:
480 label=calling_fsm.struct_name + ': ' + calling_state.short_name
481 edge_action = caller
482 if state.action == edge_action:
483 edge_action = None
Neels Hofmeyra568af22017-10-23 04:03:19 +0200484 fsm.add_special_state(fsm.states, calling_fsm.dot_name, None,
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100485 state, kind=KIND_FSM, edge_action=edge_action,
486 label=label)
487
488 # meta overview
Neels Hofmeyra568af22017-10-23 04:03:19 +0200489 meta_called_fsm = fsm_meta.have_state(fsm.dot_name, KIND_FSM)
490 meta_calling_fsm = fsm_meta.have_state(calling_fsm.dot_name, KIND_FSM)
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100491 meta_calling_fsm.add_out_edge(Edge(meta_called_fsm))
492
493
494 def have_state(fsm, name, kind=KIND_STATE):
495 state = fsm.find_state_by_name(name)
496 if not state:
497 state = State()
498 state.name = name
499 state.short_name = name
500 state.kind = kind
501 fsm.states.append(state)
502 return state
503
504 def to_dot(fsm):
505 out = ['digraph G {', 'rankdir=LR;']
506
507 for state in fsm.states:
508 out.append('%s [label="%s"%s]' % (state.short_name, state.get_label(),
509 state.shape_str()))
510
511 for state in fsm.states:
512 for out_edge in state.out_edges:
513 attrs = []
514 labels = []
515 if out_edge.events:
516 labels.extend(out_edge.event_labels())
517 if out_edge.actions:
518 labels.extend(out_edge.action_labels())
519 if labels:
520 attrs.append('label="%s"' % (r'\n'.join(labels)))
521 if out_edge.style:
522 attrs.append('style=%s'% out_edge.style)
523 attrs_str = ''
524 if attrs:
525 attrs_str = ' [%s]' % (','.join(attrs))
526 out.append('%s->%s%s' % (state.short_name, out_edge.to_state.short_name, attrs_str))
527
528 out.append('}\n')
529
530 return '\n'.join(out)
531
Neels Hofmeyra568af22017-10-23 04:03:19 +0200532 def all_names(fsm):
533 n = []
534 if fsm.from_file:
535 n.append(os.path.basename(fsm.from_file.path))
536 if fsm.struct_name:
537 n.append(fsm.struct_name)
538 if fsm.string_name:
539 n.append(fsm.string_name)
540 return n
541
542 def all_names_sanitized(fsm, sep='_'):
543 n = sep.join(fsm.all_names())
544 n = re_insane_dot_name_chars.sub('_', n)
545 return n
546
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100547 def write_dot_file(fsm):
Neels Hofmeyra568af22017-10-23 04:03:19 +0200548 dot_path = '%s.dot' % ('_'.join(fsm.all_names()))
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100549 f = open(dot_path, 'w')
550 f.write(fsm.to_dot())
551 f.close()
552 print(dot_path)
553
554
555re_fsm = re.compile(r'struct osmo_fsm ([a-z_][a-z_0-9]*) =')
Neels Hofmeyra568af22017-10-23 04:03:19 +0200556re_fsm_string_name = re.compile(r'\bname = "([^"]*)"')
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100557re_fsm_states_struct_name = re.compile(r'\bstates = ([a-z_][a-z_0-9]*)\W*,')
558re_fsm_states = re.compile(r'struct osmo_fsm_state ([a-z_][a-z_0-9]*)\[\] =')
559re_func = re.compile(r'(\b[a-z_][a-z_0-9]*\b)\([^)]*\)\W*^{', re.MULTILINE)
560re_state_trigger = re.compile(r'osmo_fsm_inst_state_chg\([^,]+,\W*([A-Z_][A-Z_0-9]*)\W*,', re.M)
561re_fsm_alloc = re.compile(r'osmo_fsm_inst_alloc[_child]*\(\W*&([a-z_][a-z_0-9]*),', re.M)
562re_fsm_event_dispatch = re.compile(r'osmo_fsm_inst_dispatch\(\W*[^,]+,\W*([A-Z_][A-Z_0-9]*)\W*,', re.M)
563
564class CFile():
565 def __init__(c_file, path):
566 c_file.path = path
567 c_file.src = open(path).read()
568 c_file.funcs = {}
569 c_file.fsm_allocators = listdict()
570
571 def extract_block(c_file, brace_open, brace_close, start):
572 pos = 0
573 try:
574 src = c_file.src
575 block_start = src.find(brace_open, start)
576
577 pos = block_start
578 level = 1
579 while level > 0:
580 pos += 1
581 if src[pos] == brace_open:
582 level += 1
583 elif src[pos] == brace_close:
584 level -= 1
585
586 return src[block_start+1:pos]
587 except:
588 print("Error while trying to extract a code block from %r char pos %d" % (c_file.path, pos))
589 print("Block start at char pos %d" % block_start)
590 try:
591 print(src[block_start - 20 : block_start + 20])
592 print('...')
593 print(src[pos - 20 : pos + 20])
594 except:
595 pass
596 return ''
597
598
599 def find_fsms(c_file):
600 fsms = []
601 for m in re_fsm.finditer(c_file.src):
602 struct_name = m.group(1)
603 struct_def = c_file.extract_block('{', '}', m.start())
Neels Hofmeyra568af22017-10-23 04:03:19 +0200604 string_name = (re_fsm_string_name.findall(struct_def) or [None])[0]
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100605 states_struct_name = re_fsm_states_struct_name.findall(struct_def)[0]
Neels Hofmeyra568af22017-10-23 04:03:19 +0200606 fsm = Fsm(struct_name, string_name, states_struct_name, c_file)
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100607 fsms.append(fsm)
608 return fsms
609
610 def find_fsm_states(c_file, fsms):
611 for m in re_fsm_states.finditer(c_file.src):
612 states_struct_name = m.group(1)
613 for fsm in fsms:
614 if states_struct_name == fsm.states_struct_name:
615 fsm.parse_states(c_file.extract_block('{', '}', m.start()))
616
617 def parse_functions(c_file):
618 funcs = {}
619 for m in re_func.finditer(c_file.src):
620 name = m.group(1)
621 func_src = c_file.extract_block('{', '}', m.start())
622 funcs[name] = func_src
623 c_file.funcs = funcs
624 c_file.find_fsm_allocators()
625
626 def find_callers(c_file, func_name):
627 func_call = func_name + '('
628 callers = []
629 for func_name, src in c_file.funcs.items():
630 if src.find(func_call) >= 0:
631 callers.append(func_name)
632 return callers
633
634 def find_fsm_allocators(c_file):
635 c_file.fsm_allocators = listdict()
636 for func_name, src in c_file.funcs.items():
637 for m in re_fsm_alloc.finditer(src):
638 fsm_struct_name = m.group(1)
639 c_file.fsm_allocators.add(fsm_struct_name, func_name)
640
641 def find_state_transitions(c_file, event_names):
642 TO_STATE = 'TO_STATE'
643 EVENT = 'EVENT'
644 func_to_state_transitions = listdict()
645
646 for func_name, src in c_file.funcs.items():
647 found_tokens = []
648
649 for m in re_state_trigger.finditer(src):
650 to_state = m.group(1)
651 found_tokens.append((m.start(), TO_STATE, to_state))
652
653 for event in event_names:
654 re_event = re.compile(r'\b(' + event + r')\b')
655 for m in re_event.finditer(src):
656 event = m.group(1)
657 found_tokens.append((m.start(), EVENT, event))
658
659 found_tokens = sorted(found_tokens)
660
661 last_event = None
662 for start, kind, name in found_tokens:
663 if kind == EVENT:
664 last_event = name
665 else:
666 func_to_state_transitions.add(func_name, (name, last_event))
667
668 return func_to_state_transitions
669
670
671 def find_event_sources(c_file, event_names):
672 c_file.event_sources = listdict()
673 for func_name, src in c_file.funcs.items():
674 for m in re_fsm_event_dispatch.finditer(src):
675 event_name = m.group(1)
676 c_file.event_sources.add(event_name, func_name)
677
678class CFiles(list):
679
680 def find_callers(c_files, func_name):
681 callers = []
682 for c_file in c_files:
683 callers.extend(c_file.find_callers(func_name))
684 return callers
685
686 def find_func_to_state_transitions(c_files):
687 func_to_state_transitions = listdict()
688 for c_file in c_files:
689 func_to_state_transitions.update( c_file.find_state_transitions(fsm.event_names) )
690 return func_to_state_transitions
691
692 def find_event_sources(c_files, event_names):
693 event_sources = listdict()
694 for c_file in c_files:
695 for event, sources in c_file.event_sources.items():
696 if event in event_names:
697 event_sources.extend(event, sources)
698 return event_sources
699
700c_files = CFiles()
701paths_seen = set()
702for path in sys.argv[1:]:
703 if path in paths_seen:
704 continue
705 paths_seen.add(path)
706 c_file = CFile(path)
707 c_files.append(c_file)
708
709for c_file in c_files:
710 c_file.parse_functions()
711
712fsms = []
713for c_file in c_files:
714 fsms.extend(c_file.find_fsms())
715
716for c_file in c_files:
717 c_file.find_fsm_states(fsms)
718 c_file.find_event_sources(fsms)
719
720for fsm in fsms:
721 fsm.find_event_edges(c_files)
722 fsm.add_fsm_alloc(c_files)
723
Neels Hofmeyra568af22017-10-23 04:03:19 +0200724fsm_meta = Fsm("meta", None, "meta")
Neels Hofmeyr0898a002016-11-16 14:36:29 +0100725for fsm in fsms:
726 fsm.add_cross_fsm_links(fsms, c_files, fsm_meta)
727
728for fsm in fsms:
729 fsm.make_events_short_names()
730
731for fsm in fsms:
732 fsm.write_dot_file()
733
734fsm_meta.write_dot_file()
735
736
737# vim: tabstop=2 shiftwidth=2 expandtab